Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e05c016
fix bug with v1 materialisation where multipled foreign keys from one…
Ewan-Keith Jan 8, 2026
1055cb0
update CHANGELOG.md
Ewan-Keith Jan 8, 2026
1d9df51
Merge branch 'main' into consistenty-persist-databricks-constraints
benc-db Jan 8, 2026
41d7de0
revert irrelavent unit test
Ewan-Keith Jan 8, 2026
e03b4c2
ammend integration test to correctly identify inc-fk -> non-inc-pk as…
Ewan-Keith Jan 8, 2026
867102e
amend changelog entry to reflect actual fix
Ewan-Keith Jan 8, 2026
ed470ee
Merge branch 'main' into consistenty-persist-databricks-constraints
Ewan-Keith Jan 9, 2026
6bb1b56
Skip test for Databricks cluster profile
Ewan-Keith Jan 9, 2026
f9a9fcb
correctly construct FK sql when using expression to specify constraint
Ewan-Keith Jan 12, 2026
90c9d97
slightly simplify string logic
Ewan-Keith Jan 12, 2026
c806a4e
Merge branch 'main' into consistenty-persist-databricks-constraints
benc-db Jan 12, 2026
84584f4
update CHANGELOG.md
Ewan-Keith Jan 8, 2026
4a88022
amend changelog entry to reflect actual fix
Ewan-Keith Jan 8, 2026
6c0e22d
move changelog entry to 1.11.5 release
Ewan-Keith Jan 12, 2026
4432f01
dont incrementally apply constraints on hive metsatore as it is not s…
Ewan-Keith Jan 12, 2026
2230aef
update CHANGELOG.md
Ewan-Keith Jan 8, 2026
1305418
amend changelog entry to reflect actual fix
Ewan-Keith Jan 8, 2026
54edbcc
add decorator to skip constraint test for databricks_cluster profile
Ewan-Keith Jan 9, 2026
b7918b7
fix ruff fmt error
Ewan-Keith Jan 14, 2026
9c81fda
update changelog
Ewan-Keith Jan 16, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Fixes

- Fix `hard_deletes: invalidate` incorrectly invalidating active records in snapshots (thanks @Zurbste!) ([#1281](https://github.com/databricks/dbt-databricks/issues/1281))
- Fix multiple foreign-keys from one table to another not being retained after incremental run

### Under the Hood

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
{% set tags = _configuration_changes.changes.get("tags", None) %}
{% set tblproperties = _configuration_changes.changes.get("tblproperties", None) %}
{% set liquid_clustering = _configuration_changes.changes.get("liquid_clustering") %}
{% set constraints = _configuration_changes.changes.get("constraints") %}
{% if tags is not none %}
{% do apply_tags(target_relation, tags.set_tags) %}
{%- endif -%}
Expand All @@ -186,6 +187,9 @@
{% if liquid_clustering is not none %}
{% do apply_liquid_clustered_cols(target_relation, liquid_clustering) %}
{% endif %}
{% if constraints %}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What i'm confused by is your description made it sound like constraints were being dropped, but we haven't removed or altered any logic that would cause them to drop. I'm digging in, but if you could provide more context, like what SQL is showing up in the logs, that would be appreciated.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By any chance, is the table with the PK not incremental? If the PK table is not incremental, during create or replace it's PK will be destroyed and recreated, which causes the cascading delete of the FKs. If the PK table were incremental, then no PKs or FKs should have been deleted. So the fix here is basically saying, incremental tables, if your constraints don't match, whether due to deletion or due to the user changing it, reapply.

@Ewan-Keith Ewan-Keith Jan 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that's it! I was having trouble replicating the case so reduced our exact issue down in the integration test, thought it was the multiple keys that were the issue but total red herring, it's the inc -> non-inc like you say.

I've updated the integration tests to reflect this and replicated the behaviour (fails on v1 before we make the change to incremental.sql, passing after).

{{ apply_constraints(target_relation, constraints) }}
{% endif %}
{%- endif -%}
{% do persist_docs(target_relation, model, for_relation=True) %}
{%- endif -%}
Expand Down
61 changes: 61 additions & 0 deletions tests/functional/adapter/incremental/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1109,3 +1109,64 @@ def model(dbt, spark):
to_columns: [id]
warn_unenforced: false
"""

target_of_multiple_fks_sql = """
{{ config(
materialized='table',
) }}

SELECT 'a' AS str_key;
"""

source_of_multiple_fks_sql = """
{{ config(
materialized='incremental',
unique_key=['fk_1_col'],
incremental_strategy='delete+insert',
on_schema_change='fail'
) }}

SELECT
'a' AS fk_1_col,
'a' AS fk_2_col
"""

multiple_fks_on_one_target_schema_yml = """
version: 2

models:
- name: target_of_multiple_fks
config:
contract:
enforced: true
columns:
- name: str_key
data_type: string
constraints:
- type: not_null
- type: primary_key
name: pk_target_key
warn_unenforced: false

- name: source_of_multiple_fks
config:
contract:
enforced: true
columns:
- name: fk_1_col
data_type: string
constraints:
- type: foreign_key
to: ref('target_of_multiple_fks')
to_columns: ["str_key"]
name: fk_1
warn_unenforced: false
- name: fk_2_col
data_type: string
constraints:
- type: foreign_key
to: ref('target_of_multiple_fks')
to_columns: ["str_key"]
name: fk_2
warn_unenforced: false
"""
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,94 @@ def test_remove_foreign_key_constraint(self, project):
util.run_dbt(["run"])
referential_constraints = project.run_sql(referential_constraint_sql, fetch="all")
assert len(referential_constraints) == 0


@pytest.mark.skip_profile("databricks_cluster")
class TestIncrementalMultipleFKsToSameTable:
"""
Test that multiple foreign keys to the same parent table persist correctly
after incremental runs. This succeeds for `use_materialization_v2` at the time
of writing, but there is a bug in the v1 materialization implementation. This
test is included to confirm this.
"""

@pytest.fixture(scope="class")
def project_config_update(self):
return {
"flags": {"use_materialization_v2": True},
}

@pytest.fixture(scope="class")
def models(self):
return {
"target_of_multiple_fks.sql": fixtures.target_of_multiple_fks_sql,
"source_of_multiple_fks.sql": fixtures.source_of_multiple_fks_sql,
"schema.yml": fixtures.multiple_fks_on_one_target_schema_yml,
}

def test_multiple_fks_to_same_table_persist_after_incremental(self, project):
expected_constraints = {
("fk_1", "pk_target_key"),
("fk_2", "pk_target_key"),
}

# Initial run - create tables with constraints
util.run_dbt(["run"])

referential_constraints = project.run_sql(referential_constraint_sql, fetch="all")

constraints = {(row[0], row[1]) for row in referential_constraints}
assert constraints == expected_constraints

# Incremental run - this should NOT lose any foreign keys
util.run_dbt(["run"])

referential_constraints_after = project.run_sql(referential_constraint_sql, fetch="all")

constraint_names_after = {(row[0], row[1]) for row in referential_constraints_after}
assert constraint_names_after == expected_constraints


@pytest.mark.skip_profile("databricks_cluster")
class TestIncrementalMultipleFKsToSameTableNoV2:
"""
Test that multiple foreign keys to the same parent table persist correctly
after incremental runs. This is a very specific test to reproduce a very
specific bug found when not using `use_materialization_v2`.
"""

@pytest.fixture(scope="class")
def project_config_update(self):
return {
"flags": {"use_materialization_v2": False},
}

@pytest.fixture(scope="class")
def models(self):
return {
"target_of_multiple_fks.sql": fixtures.target_of_multiple_fks_sql,
"source_of_multiple_fks.sql": fixtures.source_of_multiple_fks_sql,
"schema.yml": fixtures.multiple_fks_on_one_target_schema_yml,
}

def test_multiple_fks_to_same_table_persist_after_incremental(self, project):
expected_constraints = {
("fk_1", "pk_target_key"),
("fk_2", "pk_target_key"),
}

# Initial run - create tables with constraints
util.run_dbt(["run"])

referential_constraints = project.run_sql(referential_constraint_sql, fetch="all")

constraints = {(row[0], row[1]) for row in referential_constraints}
assert constraints == expected_constraints

# Incremental run - this should NOT lose any foreign keys
util.run_dbt(["run"])

referential_constraints_after = project.run_sql(referential_constraint_sql, fetch="all")

constraint_names_after = {(row[0], row[1]) for row in referential_constraints_after}
assert constraint_names_after == expected_constraints
44 changes: 44 additions & 0 deletions tests/unit/relation_configs/test_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,47 @@ def test_get_diff__check_constraints_different_formatting(self):
)
diff = config.get_diff(other)
assert diff is None

def test_get_diff__multiple_fks_to_same_table(self):
"""Test that multiple FKs to the same parent table are handled correctly."""
config = ConstraintsConfig(
set_non_nulls=set(),
set_constraints={
ForeignKeyConstraint(
type=ConstraintType.foreign_key,
name="fk_start_date",
columns=["start_date"],
to="`catalog`.`schema`.`date_dim`",
to_columns=["date_key"],
),
ForeignKeyConstraint(
type=ConstraintType.foreign_key,
name="fk_end_date",
columns=["end_date"],
to="`catalog`.`schema`.`date_dim`",
to_columns=["date_key"],
),
},
)
other = ConstraintsConfig(
set_non_nulls=set(),
set_constraints={
ForeignKeyConstraint(
type=ConstraintType.foreign_key,
name="fk_start_date",
columns=["start_date"],
to="`catalog`.`schema`.`date_dim`",
to_columns=["date_key"],
),
ForeignKeyConstraint(
type=ConstraintType.foreign_key,
name="fk_end_date",
columns=["end_date"],
to="`catalog`.`schema`.`date_dim`",
to_columns=["date_key"],
),
},
)
# Both configs have same constraints, diff should be None
diff = config.get_diff(other)
assert diff is None
Loading