Skip to content

Commit cc40cea

Browse files
authored
test: make constraints tests server-observable (add assertions, drop generated-SQL checks) (#1523)
Make the constraints functional tests assert **server-observable state** rather than generated-SQL text or run-succeeded. ## Adds server-observable assertions - **V1 contract-enforced table create** asserts PRIMARY KEY + FOREIGN KEY + NOT NULL land in information_schema (`TestV1ContractConstraintsApplied`). The V1 path was previously only generated-SQL-tested. - **type: custom** model constraint asserts delta.constraints.* via SHOW TBLPROPERTIES (`TestCustomConstraint`). - **Expression-form foreign key** asserts the referential constraint is actually created (`TestIncrementalForeignKeyExpressionConstraint` was previously assertion-free). - **Incremental NOT NULL / CHECK removal** assert the after-state (column nullable again / delta.constraints.* gone), not just that the rerun succeeds. - Enabled the previously-uncollected `TestInvalidColumnConstraints` (the test method had a leading underscore, so pytest never ran it). ## Removes brittle generated-SQL checks The `*DdlEnforcement` and quoted-column tests string-matched the generated CREATE TABLE / ALTER SQL (reading target/run/*.sql), which is brittle and outside the spirit of integration tests. Constraint SQL generation is already unit-pinned in tests/unit/macros/relations/test_constraint_macros.py, and the server effect is now covered above. Removed `TestTableConstraintsDdlEnforcement` / `TestIncrementalConstraintsDdlEnforcement` / `TestConstraintQuotedColumn` (V1 + V2), `BaseV2ConstraintSetup`, and the `expected_sql` / `expected_sql_v2` fixtures. Kept the ColumnsEqual (contract column matching) and rollback (server-observable enforcement) tests. No product code changes; tests only. ## Test plan All new/strengthened assertions verified live on databricks_uc_sql_endpoint; pre-commit (ruff / ruff-format / mypy) clean.
1 parent 860af64 commit cc40cea

6 files changed

Lines changed: 179 additions & 206 deletions

File tree

tests/functional/adapter/constraints/fixtures.py

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,5 @@
11
from dbt.tests.adapter.constraints import fixtures
22

3-
# constraints are enforced via 'alter' statements that run after table creation
4-
expected_sql = """
5-
create or replace table <model_identifier>
6-
using delta
7-
as
8-
select
9-
id,
10-
color,
11-
date_day
12-
from
13-
( select
14-
'blue' as color,
15-
1 as id,
16-
'2019-01-01' as date_day ) as model_subq
17-
"""
18-
19-
expected_sql_v2 = """
20-
create or replace table <model_identifier> (
21-
`color` string,
22-
`id` integer not null comment 'hello',
23-
`date_day` string,
24-
PRIMARY KEY (id),
25-
FOREIGN KEY (id) REFERENCES <foreign_key_model_identifier> (id)
26-
) using delta
27-
"""
28-
293
constraints_yml = fixtures.model_schema_yml.replace("text", "string").replace("primary key", "")
304

315
model_fk_constraint_schema_yml = """
@@ -318,3 +292,73 @@
318292
to: ref('parent_table')
319293
to_columns: [id]
320294
"""
295+
296+
297+
# Contract-enforced table child carrying PRIMARY KEY + FOREIGN KEY + NOT NULL, so all
298+
# three constraint kinds can be observed in information_schema after create. Reuses the
299+
# column_constraint_gate parent (a table with a PK to satisfy the FK reference).
300+
v1_contract_child_table_sql = """
301+
{{ config(materialized='table') }}
302+
select
303+
cast(1 as int) as id,
304+
cast('name' as string) as name,
305+
cast(1 as int) as parent_id
306+
"""
307+
308+
v1_contract_child_table_schema_yml = f"""
309+
version: 2
310+
models:
311+
{_column_constraint_gate_parent_model_yml} - name: v1_contract_child
312+
config:
313+
materialized: table
314+
contract:
315+
enforced: true
316+
constraints:
317+
- type: primary_key
318+
name: pk_v1_contract_child
319+
columns: ["id"]
320+
- type: foreign_key
321+
name: fk_v1_contract_child_parent
322+
columns: ["parent_id"]
323+
to: ref('parent_table')
324+
to_columns: ["id"]
325+
columns:
326+
- name: id
327+
data_type: int
328+
constraints:
329+
- type: not_null
330+
- name: name
331+
data_type: string
332+
constraints:
333+
- type: not_null
334+
- name: parent_id
335+
data_type: int
336+
constraints:
337+
- type: not_null
338+
"""
339+
340+
341+
# Contract-enforced `type: custom` constraint, rendered verbatim as
342+
# `add constraint <name> <expression>` and observable as a delta.constraints.* property.
343+
custom_constraint_model_sql = """
344+
{{ config(materialized="table") }}
345+
select 1 as id, 'blue' as color
346+
"""
347+
348+
custom_constraint_schema_yml = """
349+
version: 2
350+
models:
351+
- name: custom_constraint_model
352+
config:
353+
contract:
354+
enforced: true
355+
constraints:
356+
- type: custom
357+
name: custom_id_positive
358+
expression: "CHECK (id > 0)"
359+
columns:
360+
- name: id
361+
data_type: int
362+
- name: color
363+
data_type: string
364+
"""

tests/functional/adapter/constraints/test_column_constraint_gate.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
from dbt.tests import util
33

44
from tests.functional.adapter.constraints import fixtures
5-
from tests.functional.adapter.fixtures import MaterializationV2Mixin
5+
from tests.functional.adapter.fixtures import (
6+
MaterializationV1Mixin,
7+
MaterializationV2Mixin,
8+
RerunSafeMixin,
9+
)
610

711

812
def _constraint_rows(project, table_name, constraint_type):
@@ -128,3 +132,40 @@ def test_constraints_apply_and_survive_rerun(self, project):
128132
assert len(fk_rows_after) == 1, (
129133
f"Expected FOREIGN KEY to survive the second run, found {fk_rows_after}"
130134
)
135+
136+
137+
@pytest.mark.skip_profile("databricks_cluster")
138+
class TestV1ContractConstraintsApplied(RerunSafeMixin, MaterializationV1Mixin):
139+
@pytest.fixture(scope="class")
140+
def relations_to_reset(self):
141+
# child holds the FK to parent_table, so drop it first.
142+
return ("v1_contract_child", "parent_table")
143+
144+
@pytest.fixture(scope="class")
145+
def models(self):
146+
return {
147+
"parent_table.sql": fixtures.column_constraint_gate_parent_sql,
148+
"v1_contract_child.sql": fixtures.v1_contract_child_table_sql,
149+
"schema.yml": fixtures.v1_contract_child_table_schema_yml,
150+
}
151+
152+
def _assert_all_constraints_present(self, project):
153+
pk_rows = _constraint_rows(project, "v1_contract_child", "PRIMARY KEY")
154+
assert len(pk_rows) == 1, f"Expected one PRIMARY KEY, found {pk_rows}"
155+
assert _pk_columns(project, "v1_contract_child") == ["id"]
156+
157+
fk_rows = _constraint_rows(project, "v1_contract_child", "FOREIGN KEY")
158+
assert len(fk_rows) == 1, f"Expected one FOREIGN KEY, found {fk_rows}"
159+
160+
not_null_cols = set(_not_null_columns(project, "v1_contract_child"))
161+
assert {"id", "name", "parent_id"}.issubset(not_null_cols), (
162+
f"Expected id/name/parent_id NOT NULL, got {sorted(not_null_cols)}"
163+
)
164+
165+
def test_v1_contract_constraints_in_information_schema(self, project):
166+
util.run_dbt(["run"])
167+
self._assert_all_constraints_present(project)
168+
169+
# V1 tables recreate on every run, so the constraints must be re-applied.
170+
util.run_dbt(["run"])
171+
self._assert_all_constraints_present(project)

tests/functional/adapter/constraints/test_constraints.py

Lines changed: 34 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22
from dbt.tests import util
33
from dbt.tests.adapter.constraints import fixtures
44
from dbt.tests.adapter.constraints.test_constraints import (
5-
BaseConstraintQuotedColumn,
65
BaseConstraintsRollback,
7-
BaseConstraintsRuntimeDdlEnforcement,
86
BaseIncrementalConstraintsColumnsEqual,
97
BaseIncrementalConstraintsRollback,
10-
BaseIncrementalConstraintsRuntimeDdlEnforcement,
118
BaseTableConstraintsColumnsEqual,
129
BaseViewConstraintsColumnsEqual,
1310
)
@@ -86,41 +83,6 @@ def models(self):
8683
}
8784

8885

89-
class BaseConstraintsDdlEnforcementSetup:
90-
@pytest.fixture(scope="class")
91-
def project_config_update(self):
92-
return {"flags": {"use_materialization_v2": False}}
93-
94-
@pytest.fixture(scope="class")
95-
def expected_sql(self):
96-
return override_fixtures.expected_sql
97-
98-
99-
@pytest.mark.skip_profile("databricks_cluster")
100-
class TestTableConstraintsDdlEnforcement(
101-
BaseConstraintsDdlEnforcementSetup, BaseConstraintsRuntimeDdlEnforcement
102-
):
103-
@pytest.fixture(scope="class")
104-
def models(self):
105-
return {
106-
"my_model.sql": fixtures.my_model_wrong_order_sql,
107-
"constraints_schema.yml": override_fixtures.constraints_yml,
108-
}
109-
110-
111-
@pytest.mark.skip_profile("databricks_cluster")
112-
class TestIncrementalConstraintsDdlEnforcement(
113-
BaseConstraintsDdlEnforcementSetup,
114-
BaseIncrementalConstraintsRuntimeDdlEnforcement,
115-
):
116-
@pytest.fixture(scope="class")
117-
def models(self):
118-
return {
119-
"my_model.sql": fixtures.my_model_incremental_wrong_order_sql,
120-
"constraints_schema.yml": override_fixtures.constraints_yml,
121-
}
122-
123-
12486
class BaseConstraintsRollbackSetup:
12587
@pytest.fixture(scope="class")
12688
def project_config_update(self):
@@ -200,53 +162,59 @@ def test_incremental_foreign_key_constraint(self, project):
200162
util.run_dbt(["run", "--select", "stg_numbers"])
201163
util.run_dbt(["run", "--select", "stg_numbers"])
202164

165+
# Verify the expression-form foreign key is registered in information_schema.
166+
referential_constraints = project.run_sql(
167+
"""
168+
SELECT constraint_name
169+
FROM {database}.information_schema.referential_constraints
170+
WHERE constraint_schema = '{schema}'
171+
""",
172+
fetch="all",
173+
)
174+
assert any(row[0] == "fk_n" for row in referential_constraints), (
175+
f"expected FK 'fk_n' from the expression-form constraint, got {referential_constraints}"
176+
)
177+
203178

204179
@pytest.mark.skip_profile("databricks_cluster")
205-
class TestForeignKeyParentConstraint:
180+
class TestCustomConstraint:
206181
@pytest.fixture(scope="class")
207182
def project_config_update(self):
208183
return {"flags": {"use_materialization_v2": False}}
209184

210185
@pytest.fixture(scope="class")
211186
def models(self):
212187
return {
213-
"schema.yml": override_fixtures.parent_foreign_key,
214-
"parent_table.sql": override_fixtures.parent_sql,
215-
"child_table.sql": override_fixtures.child_sql,
188+
"custom_constraint_model.sql": override_fixtures.custom_constraint_model_sql,
189+
"schema.yml": override_fixtures.custom_constraint_schema_yml,
216190
}
217191

218-
def test_foreign_key_constraint(self, project):
219-
util.run_dbt(["build"])
192+
def test_custom_constraint_applied(self, project):
193+
util.run_dbt(["run"])
194+
rows = project.run_sql(
195+
"show tblproperties {database}.{schema}.custom_constraint_model", fetch="all"
196+
)
197+
constraints = {
198+
row.key: row.value for row in rows if row.key.startswith("delta.constraints.")
199+
}
200+
assert constraints.get("delta.constraints.custom_id_positive") == "id > 0", (
201+
f"custom constraint not persisted as expected; delta.constraints = {constraints}"
202+
)
220203

221204

222-
class TestConstraintQuotedColumn(BaseConstraintQuotedColumn):
205+
@pytest.mark.skip_profile("databricks_cluster")
206+
class TestForeignKeyParentConstraint:
223207
@pytest.fixture(scope="class")
224208
def project_config_update(self):
225209
return {"flags": {"use_materialization_v2": False}}
226210

227211
@pytest.fixture(scope="class")
228212
def models(self):
229213
return {
230-
"my_model.sql": fixtures.my_model_with_quoted_column_name_sql,
231-
"constraints_schema.yml": fixtures.model_quoted_column_schema_yml.replace(
232-
"text", "string"
233-
).replace('"from"', "`from`"),
214+
"schema.yml": override_fixtures.parent_foreign_key,
215+
"parent_table.sql": override_fixtures.parent_sql,
216+
"child_table.sql": override_fixtures.child_sql,
234217
}
235218

236-
@pytest.fixture(scope="class")
237-
def expected_sql(self):
238-
return """create or replace table <model_identifier>
239-
using delta
240-
as
241-
select
242-
id,
243-
`from`,
244-
date_day
245-
from
246-
247-
(
248-
select
249-
'blue' as `from`,
250-
1 as id,
251-
'2019-01-01' as date_day ) as model_subq
252-
"""
219+
def test_foreign_key_constraint(self, project):
220+
util.run_dbt(["build"])

0 commit comments

Comments
 (0)