Skip to content

Commit ea2d75e

Browse files
committed
fix: separate audit checks from schema nullability
1 parent cb17b96 commit ea2d75e

6 files changed

Lines changed: 69 additions & 19 deletions

File tree

‎src/sqlbuild/compiler/contracts/_helpers/columns.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
DiagnosticPhase,
2020
DiagnosticSeverity,
2121
)
22-
from sqlbuild.compiler.contracts.constants import NOT_NULL_AUDIT_NAME
2322
from sqlbuild.compiler.discovery.main._model_output_column_locations import (
2423
extract_model_output_column_locations,
2524
)
@@ -175,9 +174,7 @@ def _nullability_diagnostics(
175174

176175

177176
def _declares_not_null(column: SchemaColumn) -> bool:
178-
if column.nullable is False:
179-
return True
180-
return any(audit.definition_name == NOT_NULL_AUDIT_NAME for audit in column.audits)
177+
return column.nullable is False
181178

182179

183180
def _missing_column_diagnostic(

‎tests/integration/src/sqlbuild/cli/commands/main/_test_types.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ class SnowflakeCompileIntegrationTestCase:
5757
expected_query_fragment: str
5858

5959

60+
@dataclass(frozen=True)
61+
class ContractNullabilityCompileIntegrationTestCase:
62+
"""One contract nullability expectation through compile."""
63+
64+
description: str
65+
column_sql: str
66+
expected_exit_code: int
67+
expected_diagnostics: tuple[str, ...]
68+
69+
6070
@dataclass(frozen=True)
6171
class FormatCompileIntegrationTestCase:
6272
"""One format-to-compile compatibility expectation."""

‎tests/integration/src/sqlbuild/cli/commands/main/test_compile.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from sqlbuild.cli.commands.main.entrypoint.entry import main
1212
from tests.integration.src.sqlbuild.cli.commands.main._test_types import (
13+
ContractNullabilityCompileIntegrationTestCase,
1314
SnowflakeCompileIntegrationTestCase,
1415
)
1516

@@ -67,3 +68,47 @@ def test_given_supported_snowflake_aggregation_when_compiling_then_project_succe
6768
assert exit_code == test_case.expected_exit_code
6869
assert tuple(item["code"] for item in diagnostics) == test_case.expected_diagnostics
6970
assert test_case.expected_query_fragment in cast(str, models[0]["query_sql"])
71+
72+
73+
@pytest.mark.parametrize(
74+
"test_case",
75+
(
76+
ContractNullabilityCompileIntegrationTestCase(
77+
description="not null audit remains runtime only",
78+
column_sql="order_id (type INTEGER, audits [not_null])",
79+
expected_exit_code=0,
80+
expected_diagnostics=(),
81+
),
82+
ContractNullabilityCompileIntegrationTestCase(
83+
description="nullable false remains a schema contract",
84+
column_sql="order_id (type INTEGER, nullable false)",
85+
expected_exit_code=1,
86+
expected_diagnostics=("K004",),
87+
),
88+
),
89+
ids=lambda case: case.description,
90+
)
91+
def test_given_nullable_output_when_compiling_contract_then_only_schema_nullability_fails(
92+
test_case: ContractNullabilityCompileIntegrationTestCase,
93+
tmp_path: Path,
94+
capsys: pytest.CaptureFixture[str],
95+
) -> None:
96+
(tmp_path / "sqlbuild_project.toml").write_text(
97+
'name = "orders"\nadapter = "duckdb"\n', encoding="utf-8"
98+
)
99+
model: Path = tmp_path / "models" / "orders.sql"
100+
model.parent.mkdir()
101+
model.write_text(
102+
"MODEL (\n"
103+
" contract enforced,\n"
104+
f" columns ({test_case.column_sql}),\n"
105+
");\n\n"
106+
"SELECT CAST(NULL AS INTEGER) AS order_id\n",
107+
encoding="utf-8",
108+
)
109+
exit_code: int = main(["--project-dir", str(tmp_path), "compile", "--json", "--no-cache"])
110+
result: dict[str, object] = json.loads(capsys.readouterr().out)
111+
diagnostics: list[dict[str, object]] = cast(list[dict[str, object]], result["diagnostics"])
112+
113+
assert exit_code == test_case.expected_exit_code
114+
assert tuple(item["code"] for item in diagnostics) == test_case.expected_diagnostics

‎tests/unit/src/sqlbuild/compiler/contracts/_test_types.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class ContractValidationTestCase:
1717
expected_messages: tuple[str, ...]
1818
contract: str | None = None
1919
column_contract_mode: str = "implicit"
20-
declared_not_null_columns: tuple[str, ...] = ()
20+
not_null_audit_columns: tuple[str, ...] = ()
2121
declared_nullable_by_column: dict[str, bool | None] | None = None
2222
inferred_nullability_by_column: dict[str, InferredNullability] | None = None
2323
fast_lineage_has_star: bool = False

‎tests/unit/src/sqlbuild/compiler/contracts/helpers.py‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def make_contract_project(
3232
column_contract_mode: str = "implicit",
3333
model_name: str = "orders",
3434
column_locations: dict[str, SourceLocation] | None = None,
35-
declared_not_null_columns: tuple[str, ...] = (),
35+
not_null_audit_columns: tuple[str, ...] = (),
3636
declared_nullable_by_column: dict[str, bool | None] | None = None,
3737
inferred_nullability_by_column: dict[str, InferredNullability] | None = None,
3838
fast_lineage_has_star: bool = False,
@@ -78,7 +78,7 @@ def make_contract_project(
7878
type=column_type,
7979
audits=_column_audits(
8080
name=name,
81-
declared_not_null_columns=declared_not_null_columns,
81+
not_null_audit_columns=not_null_audit_columns,
8282
),
8383
nullable=(declared_nullable_by_column or {}).get(name),
8484
location=(column_locations or {}).get(name),
@@ -107,8 +107,6 @@ def make_contract_project(
107107

108108

109109
def _column_audits(
110-
*, name: str, declared_not_null_columns: tuple[str, ...]
110+
*, name: str, not_null_audit_columns: tuple[str, ...]
111111
) -> tuple[SchemaAuditInstance, ...]:
112-
return ((), (SchemaAuditInstance(definition_name="not_null"),))[
113-
name in declared_not_null_columns
114-
]
112+
return ((), (SchemaAuditInstance(definition_name="not_null"),))[name in not_null_audit_columns]

‎tests/unit/src/sqlbuild/compiler/contracts/test_validate.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,9 @@
206206
),
207207
),
208208
ContractValidationTestCase(
209-
description="declared not null ignores unknown inferred nullability",
209+
description="not null audit ignores unknown inferred nullability",
210210
declared_columns=(("order_id", None),),
211-
declared_not_null_columns=("order_id",),
211+
not_null_audit_columns=("order_id",),
212212
inferred_columns=(("order_id", None),),
213213
inferred_nullability_by_column={"order_id": InferredNullability.UNKNOWN},
214214
type_enforcement=None,
@@ -217,15 +217,15 @@
217217
expected_messages=(),
218218
),
219219
ContractValidationTestCase(
220-
description="declared not null fails when output is proven nullable",
220+
description="not null audit does not declare schema nullability",
221221
declared_columns=(("customer_name", None),),
222-
declared_not_null_columns=("customer_name",),
222+
not_null_audit_columns=("customer_name",),
223223
inferred_columns=(("customer_name", None),),
224224
inferred_nullability_by_column={"customer_name": InferredNullability.NULLABLE},
225225
type_enforcement=None,
226-
expected_codes=("K004",),
227-
expected_severities=("error",),
228-
expected_messages=("column 'customer_name' is declared non-null but may be nullable",),
226+
expected_codes=(),
227+
expected_severities=(),
228+
expected_messages=(),
229229
),
230230
ContractValidationTestCase(
231231
description="declared nullable false fails when output is proven nullable",
@@ -260,7 +260,7 @@ def test_given_compiled_project_when_validating_contracts_then_returns_expected_
260260
declared_columns=test_case.declared_columns,
261261
inferred_columns=test_case.inferred_columns,
262262
type_enforcement=test_case.type_enforcement,
263-
declared_not_null_columns=test_case.declared_not_null_columns,
263+
not_null_audit_columns=test_case.not_null_audit_columns,
264264
declared_nullable_by_column=test_case.declared_nullable_by_column,
265265
inferred_nullability_by_column=test_case.inferred_nullability_by_column,
266266
fast_lineage_has_star=test_case.fast_lineage_has_star,

0 commit comments

Comments
 (0)