Repository: databricks/dbt-databricks (python)

Issue:
Cannot update models with primary & foreign key constraints since version 1.10.2 with MaterializationV2
### Describe the bug

Primary key constraints fail to be dropped when there are dependent foreign key constraints.

```
Database Error in model .....
  Cannot delete constraint `..._pk` with non empty child constraints. Set the argument CASCADE to true to delete both this constraint and all child constraints.
  compiled code at target/run/.....
```

### Steps To Reproduce
Create two models, give one a primary key, and the other a foreign key, make it depend on the model with the primary key. Now run dbt and watch it fail to drop the primary key constraint.

### Expected behavior
This SQL should have `CASCADE` at the end when a primary key constraint is dropped, so that foreign key constraints depending on it are dropped as well.

https://github.com/databricks/dbt-databricks/blob/df3d0f6b6ecd7976d365878742e74403afb1ca68/dbt/include/databricks/macros/relations/components/constraints.sql#L133-L135

### System information
```
Core:
  - installed: 1.10.2
  - latest:    1.10.2 - Up to date!

Plugins:
  - spark:      1.9.2  - Up to date!
  - databricks: 1.10.4 - Up to date!
```

`OS: WSL`

`Python 3.12.10`

### Additional context

With MaterializationV2 enabled, the last working configuration is:

```
dbt-core: 1.10.2
dbt-databricks: 1.10.1
```

If I disable MaterializationV2 I can use the latest version of dbt-databricks (1.10.4).

Pull request description:
Fix TypedConstraint equality check
<!-- Please review our pull request review process in CONTRIBUTING.md before your proceed. -->

Resolves #1081

<!---
  Include the number of the issue addressed by this PR above if applicable.

  Example:
    resolves #1234

  Please review our pull request review process in CONTRIBUTING.md before your proceed.
-->

### Description

It looks like the root cause behind #1081 is because of how we determine equality between TypedConstraint objects. During incremental runs, we rely on the equality check to determine which constraints to drop or apply https://github.com/databricks/dbt-databricks/blob/6981678333b6f1b715b272041b166576df925e37/dbt/adapters/databricks/relation_configs/constraints.py#L30-L32

The current default equality check is considering fields like `warn_unenforced`. This does not work because this is a user supplied config that has no meaning when we are creating a `TypedConstraint` from existing Databricks relations. The Fix is to override the `__eq__` to match the `__hash__` which already ignores such fields

### Checklist

- [ ] I have run this code in development and it appears to resolve the stated issue
- [ ] This PR includes tests, or tests are not required/relevant for this PR
- [ ] I have updated the `CHANGELOG.md` and added information about my change to the "dbt-databricks next" section.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d055227d..2f1b0b58 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ### Fixes
 
 - Fix inefficient query when getting column schema for MV/STs ([1074](https://github.com/databricks/dbt-databricks/issues/1074))
+- Fix bug causing false positives in diffing constraints between existing relation and model config for incremental runs ([1081](https://github.com/databricks/dbt-databricks/issues/1081))
 
 ## dbt-databricks 1.10.4 (June 24, 2025)
 
diff --git a/dbt/adapters/databricks/constraints.py b/dbt/adapters/databricks/constraints.py
index 8ecded5b..44c36d79 100644
--- a/dbt/adapters/databricks/constraints.py
+++ b/dbt/adapters/databricks/constraints.py
@@ -76,6 +76,14 @@ class TypedConstraint(ModelLevelConstraint, ABC):
         )
         return hash(fields)
 
+    def __eq__(self, other: Any) -> bool:
+        """Override equality to only compare fields used in hash calculation.
+
+        This ensures hash/equality contract is maintained and prevents issues
+        with set operations when warn_unenforced/warn_unsupported differ.
+        """
+        return self.__hash__() == other.__hash__()
+
 
 class CustomConstraint(TypedConstraint):
     str_type = "custom"


Test patch:
diff --git a/tests/functional/adapter/incremental/fixtures.py b/tests/functional/adapter/incremental/fixtures.py
index 8966ea3b..368c571f 100644
--- a/tests/functional/adapter/incremental/fixtures.py
+++ b/tests/functional/adapter/incremental/fixtures.py
@@ -1011,3 +1011,43 @@ def model(dbt, spark):
     data = [[1, 'hello', 'blue']]
     return spark.createDataFrame(data, schema=['id', 'msg', 'color'])
 """
+
+warn_unenforced_override_sql = """
+select "abc" as id
+"""
+
+warn_unenforced_override_model = """
+version: 2
+
+models:
+  - name: model_a
+    config:
+      materialized: incremental
+      on_schema_change: fail
+    columns:
+      - name: id
+        data_type: string
+        constraints:
+          - type: not_null
+    constraints:
+      - type: primary_key
+        columns:
+          - id
+        name: model_a_pk
+        warn_unenforced: false
+
+  - name: model_b
+    config:
+        materialized: table
+    columns:
+      - name: id
+        data_type: string
+    constraints:
+      - type: foreign_key
+        columns:
+          - id
+        name: model_b_fk
+        to: ref('model_a')
+        to_columns: [id]
+        warn_unenforced: false
+"""
diff --git a/tests/functional/adapter/incremental/test_incremental_constraints.py b/tests/functional/adapter/incremental/test_incremental_constraints.py
index ff456b9a..66a627e0 100644
--- a/tests/functional/adapter/incremental/test_incremental_constraints.py
+++ b/tests/functional/adapter/incremental/test_incremental_constraints.py
@@ -217,6 +217,36 @@ class TestIncrementalSetForeignKeyConstraint:
         assert constraint_pairs == expected_pairs
 
 
+@pytest.mark.skip_profile("databricks_cluster")
+class TestIncrementalDiff:
+    @pytest.fixture(scope="class")
+    def project_config_update(self):
+        return {
+            "flags": {"use_materialization_v2": True},
+        }
+
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "model_a.sql": fixtures.warn_unenforced_override_sql,
+            "model_b.sql": fixtures.warn_unenforced_override_sql,
+            "schema.yml": fixtures.warn_unenforced_override_model,
+        }
+
+    # Specifically for testing bugs like https://github.com/databricks/dbt-databricks/issues/1081
+    # where the config diff between the existing relation and model definition incorrectly detected
+    # constraints that were not changed. This is because the TypedConstraint read from existing
+    # Databricks relations will just have a default value for warn_unenforced which should
+    # be ignored during the diff
+    def test_warn_unenforced_false(self, project):
+        referential_constraints = project.run_sql(referential_constraint_sql, fetch="all")
+        assert len(referential_constraints) == 0
+        util.run_dbt(["run"])
+        util.run_dbt(["run"])
+        referential_constraints = project.run_sql(referential_constraint_sql, fetch="all")
+        assert len(referential_constraints) == 1
+
+
 @pytest.mark.skip_profile("databricks_cluster")
 class TestIncrementalRemoveForeignKeyConstraint:
     @pytest.fixture(scope="class")
diff --git a/tests/unit/relation_configs/test_constraint.py b/tests/unit/relation_configs/test_constraint.py
index 9247f81e..187fdf2d 100644
--- a/tests/unit/relation_configs/test_constraint.py
+++ b/tests/unit/relation_configs/test_constraint.py
@@ -281,6 +281,7 @@ class TestConstraintsConfig:
                     type=ConstraintType.check,
                     name="check_name_length",
                     expression="LENGTH (name) >= 1",
+                    warn_unenforced=False,
                 ),
                 PrimaryKeyConstraint(
                     type=ConstraintType.primary_key,
@@ -296,6 +297,8 @@ class TestConstraintsConfig:
                     type=ConstraintType.check,
                     name="check_name_length",
                     expression="LENGTH (name) >= 1",
+                    # This should be ignored by the TypedConstraint equality check
+                    warn_unenforced=True,
                 ),
                 PrimaryKeyConstraint(
                     type=ConstraintType.primary_key,