Repository: dbt-labs/dbt-core

Issue:
[CT-3544] [Bug] Model level constraints aren't being applied to versioned models
### Is this a new bug in dbt-core?

- [X] I believe this is a new bug in dbt-core
- [X] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

If you're using model versions (https://docs.getdbt.com/docs/collaborate/govern/model-versions), model level constraints (https://docs.getdbt.com/reference/resource-properties/constraints#defining-constraints) don't seem to be applied to the model.

### Expected Behavior

Model level constraints should be applied as expected.

### Steps To Reproduce

First let's look at a well behaved model level constraints example (without model versioning).

```yaml
# dbt_project.yml
name: my_dbt_project
profile: all
config-version: 2
version: "1.0.0"

models:
  my_dbt_project:
    +materialized: table

# models/schema.yml
version: 2
models:
  - name: foo
    config:
      contract:
        enforced: true
    constraints:
      - type: primary_key
        columns: [id, user_name]
    columns:
      - name: id
        data_type: int
        constraints:
          - type: not_null
      - name: user_name
        data_type: text
```

```sql
-- models/foo.sql
select 1 as id, 'alice' as user_name
```

Do a run:

```sh
$ dbt --debug run -s foo
...
23:06:58  On model.my_dbt_project.foo: /* {"app": "dbt", "dbt_version": "1.7.4", "profile_name": "all", "target_name": "pg-local", "node_id": "model.my_dbt_project.foo"} */

  
    

  create  table "postgres"."public"."foo__dbt_tmp"
  
  
    
  
  (
    id int not null,
    user_name text,
    
    primary key (id, user_name)
    )
 ;
    insert into "postgres"."public"."foo__dbt_tmp" (
      id, user_name
    )
  
  (
    
    select id, user_name
    from (
        select 1 as id, 'alice' as user_name
    ) as model_subq
  );
  
23:06:58  SQL status: INSERT 0 1 in 0.0 seconds
...
```

We see the constraint `primary key (id, user_name)` is applied.

Now let's rename the file `foo.sql` > `foo_v1.sql` and edit the `schema.yml` files contents:

```yaml
# models/schema.yml
version: 2
models:
  - name: foo
    latest_version: 1
    config:
      contract:
        enforced: true
    constraints:
      - type: primary_key
        columns: [id, user_name]
    columns:
      - name: id
        data_type: int
        constraints:
          - type: not_null
      - name: user_name
        data_type: text
    versions:
      - v: 1
```

```sql
-- models/foo_v1.sql
select 1 as id, 'alice' as user_name
```

Do a run:

```sh
$ dbt clean && dbt --debug run -s foo
...
23:09:56  On model.my_dbt_project.foo.v1: /* {"app": "dbt", "dbt_version": "1.7.4", "profile_name": "all", "target_name": "pg-local", "node_id": "model.my_dbt_project.foo.v1"} */

  
    

  create  table "postgres"."public"."foo_v1__dbt_tmp"
  
  
    
  
  (
    id int not null,
    user_name text
    
    )
 ;
    insert into "postgres"."public"."foo_v1__dbt_tmp" (
      id, user_name
    )
  
  (
    
    select id, user_name
    from (
        select 1 as id, 'alice' as user_name
    ) as model_subq
  );
  
23:09:56  SQL status: INSERT 0 1 in 0.0 seconds
...
```

A versioned model of `foo` is created (`foo_v1`) and column level contraints are applied but the model level constraint is not applied.


### Relevant log output

_No response_

### Environment

```markdown
- OS: macOS
- Python: 3.11.2
- dbt: 
Core:
  - installed: 1.7.4
  - latest:    1.7.4 - Up to date!

Plugins:
  - bigquery:  1.7.2 - Up to date!
  - snowflake: 1.7.1 - Up to date!
  - postgres:  1.7.4 - Up to date!
```


### Which database adapter are you using with dbt?

postgres

### Additional Context

_No response_


Discussion:

Resolving patch:
diff --git a/core/dbt/parser/schemas.py b/core/dbt/parser/schemas.py
--- a/core/dbt/parser/schemas.py
+++ b/core/dbt/parser/schemas.py
@@ -845,7 +845,10 @@ def parse_patch(self, block: TargetBlock[UnparsedModelUpdate], refs: ParserRef)
                 # Includes alias recomputation
                 self.patch_node_config(versioned_model_node, versioned_model_patch)
 
-                # Need to reapply this here, in the case that 'contract: {enforced: true}' was during config-setting
+                # Need to reapply setting constraints and contract checksum here, because
+                # they depend on node.contract.enabled, which wouldn't be set when
+                # patch_node_properties was called if it wasn't set in the model file.
+                self.patch_constraints(versioned_model_node, versioned_model_patch.constraints)
                 versioned_model_node.build_contract_checksum()
                 source_file.append_patch(
                     versioned_model_patch.yaml_key, versioned_model_node.unique_id
@@ -869,6 +872,7 @@ def patch_node_properties(self, node, patch: "ParsedNodePatch"):
                     unique_id=node.unique_id,
                     field_value=patch.access,
                 )
+        # These two will have to be reapplied after config is built for versioned models
         self.patch_constraints(node, patch.constraints)
         node.build_contract_checksum()
 


Test patch:
diff --git a/core/dbt/tests/__init__.py b/core/dbt/tests/__init__.py
deleted file mode 100644
diff --git a/tests/functional/configs/test_contract_configs.py b/tests/functional/configs/test_contract_configs.py
--- a/tests/functional/configs/test_contract_configs.py
+++ b/tests/functional/configs/test_contract_configs.py
@@ -85,7 +85,6 @@ def model(dbt, _):
 """
 
 model_schema_yml = """
-version: 2
 models:
   - name: my_model
     config:
@@ -110,7 +109,6 @@ def model(dbt, _):
 """
 
 model_schema_alias_types_false_yml = """
-version: 2
 models:
   - name: my_model
     config:
@@ -136,7 +134,6 @@ def model(dbt, _):
 """
 
 model_schema_ignore_unsupported_yml = """
-version: 2
 models:
   - name: my_model
     config:
@@ -164,7 +161,6 @@ def model(dbt, _):
 """
 
 model_schema_errors_yml = """
-version: 2
 models:
   - name: my_model
     config:
@@ -206,7 +202,6 @@ def model(dbt, _):
 """
 
 model_schema_blank_yml = """
-version: 2
 models:
   - name: my_model
     config:
@@ -215,7 +210,6 @@ def model(dbt, _):
 """
 
 model_schema_complete_datatypes_yml = """
-version: 2
 models:
   - name: my_model
     columns:
@@ -237,7 +231,6 @@ def model(dbt, _):
 """
 
 model_schema_incomplete_datatypes_yml = """
-version: 2
 models:
   - name: my_model
     columns:
diff --git a/tests/functional/configs/test_versioned_model_constraint.py b/tests/functional/configs/test_versioned_model_constraint.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/configs/test_versioned_model_constraint.py
@@ -0,0 +1,76 @@
+import pytest
+from dbt.tests.util import run_dbt, rm_file, write_file, get_manifest
+
+
+schema_yml = """
+models:
+  - name: foo
+    config:
+      materialized: table
+      contract:
+        enforced: true
+    constraints:
+      - type: primary_key
+        columns: [id, user_name]
+    columns:
+      - name: id
+        data_type: int
+        constraints:
+          - type: not_null
+      - name: user_name
+        data_type: text
+"""
+
+foo_sql = """
+select 1 as id, 'alice' as user_name
+"""
+
+versioned_schema_yml = """
+models:
+  - name: foo
+    latest_version: 1
+    config:
+      materialized: table
+      contract:
+        enforced: true
+    constraints:
+      - type: primary_key
+        columns: [id, user_name]
+    columns:
+      - name: id
+        data_type: int
+        constraints:
+          - type: not_null
+      - name: user_name
+        data_type: text
+    versions:
+      - v: 1
+"""
+
+
+class TestVersionedModelConstraints:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {
+            "foo.sql": foo_sql,
+            "schema.yml": schema_yml,
+        }
+
+    def test_versioned_model_constraints(self, project):
+        results = run_dbt(["run"])
+        assert len(results) == 1
+        manifest = get_manifest(project.project_root)
+        model_node = manifest.nodes["model.test.foo"]
+        assert len(model_node.constraints) == 1
+
+        # remove foo.sql and create foo_v1.sql
+        rm_file(project.project_root, "models", "foo.sql")
+        write_file(foo_sql, project.project_root, "models", "foo_v1.sql")
+        write_file(versioned_schema_yml, project.project_root, "models", "schema.yml")
+        results = run_dbt(["run"])
+        assert len(results) == 1
+
+        manifest = get_manifest(project.project_root)
+        model_node = manifest.nodes["model.test.foo.v1"]
+        assert model_node.contract.enforced is True
+        assert len(model_node.constraints) == 1