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
5 changes: 1 addition & 4 deletions src/sqlbuild/compiler/contracts/_helpers/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
DiagnosticPhase,
DiagnosticSeverity,
)
from sqlbuild.compiler.contracts.constants import NOT_NULL_AUDIT_NAME
from sqlbuild.compiler.discovery.main._model_output_column_locations import (
extract_model_output_column_locations,
)
Expand Down Expand Up @@ -175,9 +174,7 @@ def _nullability_diagnostics(


def _declares_not_null(column: SchemaColumn) -> bool:
if column.nullable is False:
return True
return any(audit.definition_name == NOT_NULL_AUDIT_NAME for audit in column.audits)
return column.nullable is False


def _missing_column_diagnostic(
Expand Down
10 changes: 10 additions & 0 deletions tests/integration/src/sqlbuild/cli/commands/main/_test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ class SnowflakeCompileIntegrationTestCase:
expected_query_fragment: str


@dataclass(frozen=True)
class ContractNullabilityCompileIntegrationTestCase:
"""One contract nullability expectation through compile."""

description: str
column_sql: str
expected_exit_code: int
expected_diagnostics: tuple[str, ...]


@dataclass(frozen=True)
class FormatCompileIntegrationTestCase:
"""One format-to-compile compatibility expectation."""
Expand Down
45 changes: 45 additions & 0 deletions tests/integration/src/sqlbuild/cli/commands/main/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from sqlbuild.cli.commands.main.entrypoint.entry import main
from tests.integration.src.sqlbuild.cli.commands.main._test_types import (
ContractNullabilityCompileIntegrationTestCase,
SnowflakeCompileIntegrationTestCase,
)

Expand Down Expand Up @@ -67,3 +68,47 @@ def test_given_supported_snowflake_aggregation_when_compiling_then_project_succe
assert exit_code == test_case.expected_exit_code
assert tuple(item["code"] for item in diagnostics) == test_case.expected_diagnostics
assert test_case.expected_query_fragment in cast(str, models[0]["query_sql"])


@pytest.mark.parametrize(
"test_case",
(
ContractNullabilityCompileIntegrationTestCase(
description="not null audit remains runtime only",
column_sql="order_id (type INTEGER, audits [not_null])",
expected_exit_code=0,
expected_diagnostics=(),
),
ContractNullabilityCompileIntegrationTestCase(
description="nullable false remains a schema contract",
column_sql="order_id (type INTEGER, nullable false)",
expected_exit_code=1,
expected_diagnostics=("K004",),
),
),
ids=lambda case: case.description,
)
def test_given_nullable_output_when_compiling_contract_then_only_schema_nullability_fails(
test_case: ContractNullabilityCompileIntegrationTestCase,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
(tmp_path / "sqlbuild_project.toml").write_text(
'name = "orders"\nadapter = "duckdb"\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.column_sql}),\n"
");\n\n"
"SELECT CAST(NULL AS INTEGER) AS order_id\n",
encoding="utf-8",
)
exit_code: int = main(["--project-dir", str(tmp_path), "compile", "--json", "--no-cache"])
result: dict[str, object] = json.loads(capsys.readouterr().out)
diagnostics: list[dict[str, object]] = cast(list[dict[str, object]], result["diagnostics"])

assert exit_code == test_case.expected_exit_code
assert tuple(item["code"] for item in diagnostics) == test_case.expected_diagnostics
2 changes: 1 addition & 1 deletion tests/unit/src/sqlbuild/compiler/contracts/_test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class ContractValidationTestCase:
expected_messages: tuple[str, ...]
contract: str | None = None
column_contract_mode: str = "implicit"
declared_not_null_columns: tuple[str, ...] = ()
not_null_audit_columns: tuple[str, ...] = ()
declared_nullable_by_column: dict[str, bool | None] | None = None
inferred_nullability_by_column: dict[str, InferredNullability] | None = None
fast_lineage_has_star: bool = False
Expand Down
10 changes: 4 additions & 6 deletions tests/unit/src/sqlbuild/compiler/contracts/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def make_contract_project(
column_contract_mode: str = "implicit",
model_name: str = "orders",
column_locations: dict[str, SourceLocation] | None = None,
declared_not_null_columns: tuple[str, ...] = (),
not_null_audit_columns: tuple[str, ...] = (),
declared_nullable_by_column: dict[str, bool | None] | None = None,
inferred_nullability_by_column: dict[str, InferredNullability] | None = None,
fast_lineage_has_star: bool = False,
Expand Down Expand Up @@ -78,7 +78,7 @@ def make_contract_project(
type=column_type,
audits=_column_audits(
name=name,
declared_not_null_columns=declared_not_null_columns,
not_null_audit_columns=not_null_audit_columns,
),
nullable=(declared_nullable_by_column or {}).get(name),
location=(column_locations or {}).get(name),
Expand Down Expand Up @@ -107,8 +107,6 @@ def make_contract_project(


def _column_audits(
*, name: str, declared_not_null_columns: tuple[str, ...]
*, name: str, not_null_audit_columns: tuple[str, ...]
) -> tuple[SchemaAuditInstance, ...]:
return ((), (SchemaAuditInstance(definition_name="not_null"),))[
name in declared_not_null_columns
]
return ((), (SchemaAuditInstance(definition_name="not_null"),))[name in not_null_audit_columns]
16 changes: 8 additions & 8 deletions tests/unit/src/sqlbuild/compiler/contracts/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,9 @@
),
),
ContractValidationTestCase(
description="declared not null ignores unknown inferred nullability",
description="not null audit ignores unknown inferred nullability",
declared_columns=(("order_id", None),),
declared_not_null_columns=("order_id",),
not_null_audit_columns=("order_id",),
inferred_columns=(("order_id", None),),
inferred_nullability_by_column={"order_id": InferredNullability.UNKNOWN},
type_enforcement=None,
Expand All @@ -217,15 +217,15 @@
expected_messages=(),
),
ContractValidationTestCase(
description="declared not null fails when output is proven nullable",
description="not null audit does not declare schema nullability",
declared_columns=(("customer_name", None),),
declared_not_null_columns=("customer_name",),
not_null_audit_columns=("customer_name",),
inferred_columns=(("customer_name", None),),
inferred_nullability_by_column={"customer_name": InferredNullability.NULLABLE},
type_enforcement=None,
expected_codes=("K004",),
expected_severities=("error",),
expected_messages=("column 'customer_name' is declared non-null but may be nullable",),
expected_codes=(),
expected_severities=(),
expected_messages=(),
),
ContractValidationTestCase(
description="declared nullable false fails when output is proven nullable",
Expand Down Expand Up @@ -260,7 +260,7 @@ def test_given_compiled_project_when_validating_contracts_then_returns_expected_
declared_columns=test_case.declared_columns,
inferred_columns=test_case.inferred_columns,
type_enforcement=test_case.type_enforcement,
declared_not_null_columns=test_case.declared_not_null_columns,
not_null_audit_columns=test_case.not_null_audit_columns,
declared_nullable_by_column=test_case.declared_nullable_by_column,
inferred_nullability_by_column=test_case.inferred_nullability_by_column,
fast_lineage_has_star=test_case.fast_lineage_has_star,
Expand Down
Loading