Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ce9f341
CT-1922: Rough in functionality for parsing model level constraints
peterallenwebb Mar 27, 2023
f3bd5f3
CT-1922: (Almost) complete support for model level constraints
peterallenwebb Mar 29, 2023
5699852
CT-1922: Fix typo affecting correct model constraint parsing.
peterallenwebb Mar 29, 2023
d9197c7
CT-1922: Rework base class for model tests for greater simplicity
peterallenwebb Apr 5, 2023
02c94ed
CT-1922: Rough in functionality for parsing model level constraints
peterallenwebb Mar 27, 2023
7b85bf6
CT-1922: Revise unit tests for new model-level constraints property
peterallenwebb Mar 28, 2023
bf614ba
CT-1922: (Almost) complete support for model level constraints
peterallenwebb Mar 29, 2023
2198c4c
first pass
emmyoop Mar 28, 2023
edda34e
implement in core
emmyoop Mar 29, 2023
a920bfe
add proto
emmyoop Mar 29, 2023
620023f
WIP
emmyoop Mar 31, 2023
27d074e
resolve errors in columns_spec_ddl
emmyoop Mar 31, 2023
a94ca62
changelog
emmyoop Mar 31, 2023
24c5a56
update comment
emmyoop Mar 31, 2023
9daada8
move logic over to python
emmyoop Apr 3, 2023
253dc9f
rename and use enum
emmyoop Apr 4, 2023
cf5a6ee
update default constraint_support dict
emmyoop Apr 4, 2023
dd49aa2
generate new proto definition after conflicts
emmyoop Apr 4, 2023
ee00d36
reorganize code and break warnings into each constraint
emmyoop Apr 4, 2023
6f18100
fix postgres constraint support
emmyoop Apr 4, 2023
47d6bdc
remove breakpoint
emmyoop Apr 4, 2023
51e71b5
convert constraint support to constant
emmyoop Apr 4, 2023
d9603db
update postgres
emmyoop Apr 4, 2023
42d90d2
add to export
emmyoop Apr 4, 2023
2d2f277
add to export
emmyoop Apr 4, 2023
ad0b4a4
regen proto types file
emmyoop Apr 6, 2023
35d3307
standardize names
emmyoop Apr 7, 2023
146ebb3
put back mypy error
emmyoop Apr 7, 2023
3058822
more naming + add back comma
emmyoop Apr 7, 2023
3e3b8a3
add constraint support to model level constraints
emmyoop Apr 7, 2023
4bed983
update event message and method signature
emmyoop Apr 10, 2023
15101f1
rename method
emmyoop Apr 10, 2023
4e2c174
CT-1922: Rough in functionality for parsing model level constraints
peterallenwebb Mar 27, 2023
432ef9a
CT-1922: Revise unit tests for new model-level constraints property
peterallenwebb Mar 28, 2023
f1d4c18
CT-1922: (Almost) complete support for model level constraints
peterallenwebb Mar 29, 2023
864554d
CT-1922: Fix typo affecting correct model constraint parsing.
peterallenwebb Mar 29, 2023
ae94f48
CT-1922: Improve whitespace handling
peterallenwebb Mar 31, 2023
de4e163
CT-1922: Render raw constraints to constraint list directly
peterallenwebb Apr 5, 2023
9c79a0d
make method return consistent
emmyoop Apr 10, 2023
276d0c3
regenerate proto defn
emmyoop Apr 10, 2023
044140b
update evvent test
emmyoop Apr 10, 2023
bae80b0
add some code cleanup
emmyoop Apr 11, 2023
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
6 changes: 6 additions & 0 deletions .changes/unreleased/Under the Hood-20230331-132119.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Under the Hood
body: Generalize constraint compatibility warnings
time: 2023-03-31T13:21:19.507995-05:00
custom:
Author: emmyoop
Issue: "7067"
19 changes: 12 additions & 7 deletions core/dbt/adapters/base/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# these are all just exports, #noqa them so flake8 will be happy

# TODO: Should we still include this in the `adapters` namespace?
from dbt.contracts.connection import Credentials # noqa
from dbt.adapters.base.meta import available # noqa
from dbt.adapters.base.connections import BaseConnectionManager # noqa
from dbt.adapters.base.relation import ( # noqa
from dbt.contracts.connection import Credentials # noqa: F401
from dbt.adapters.base.meta import available # noqa: F401
from dbt.adapters.base.connections import BaseConnectionManager # noqa: F401
from dbt.adapters.base.relation import ( # noqa: F401
BaseRelation,
RelationType,
SchemaSearchMap,
)
from dbt.adapters.base.column import Column # noqa
from dbt.adapters.base.impl import AdapterConfig, BaseAdapter, PythonJobHelper # noqa
from dbt.adapters.base.plugin import AdapterPlugin # noqa
from dbt.adapters.base.column import Column # noqa: F401
from dbt.adapters.base.impl import ( # noqa: F401
AdapterConfig,
BaseAdapter,
PythonJobHelper,
ConstraintSupport,
)
from dbt.adapters.base.plugin import AdapterPlugin # noqa: F401
69 changes: 60 additions & 9 deletions core/dbt/adapters/base/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from concurrent.futures import as_completed, Future
from contextlib import contextmanager
from datetime import datetime
from enum import Enum
import time
from itertools import chain
from typing import (
Expand All @@ -16,6 +17,7 @@
Set,
Tuple,
Type,
Union,
)

from dbt.contracts.graph.nodes import ColumnLevelConstraint, ConstraintType, ModelLevelConstraint
Expand Down Expand Up @@ -53,6 +55,8 @@
CodeExecution,
CodeExecutionStatus,
CatalogGenerationError,
ConstraintNotSupported,
ConstraintNotEnforced,
)
from dbt.utils import filter_null_values, executor, cast_to_str, AttrDict

Expand All @@ -73,6 +77,12 @@
FRESHNESS_MACRO_NAME = "collect_freshness"


class ConstraintSupport(str, Enum):
ENFORCED = "enforced"
NOT_ENFORCED = "not_enforced"
NOT_SUPPORTED = "not_supported"


def _expect_row_value(key: str, row: agate.Row):
if key not in row.keys():
raise DbtInternalError(
Expand Down Expand Up @@ -204,6 +214,14 @@ class BaseAdapter(metaclass=AdapterMeta):
# for use in materializations
AdapterSpecificConfigs: Type[AdapterConfig] = AdapterConfig

CONSTRAINT_SUPPORT = {
ConstraintType.check: ConstraintSupport.NOT_SUPPORTED,
ConstraintType.not_null: ConstraintSupport.ENFORCED,
ConstraintType.unique: ConstraintSupport.NOT_ENFORCED,
ConstraintType.primary_key: ConstraintSupport.NOT_ENFORCED,
ConstraintType.foreign_key: ConstraintSupport.ENFORCED,
}

def __init__(self, config):
self.config = config
self.cache = RelationsCache()
Expand Down Expand Up @@ -1273,14 +1291,8 @@ def _parse_column_constraint(cls, raw_constraint: Dict[str, Any]) -> ColumnLevel
except Exception:
raise DbtValidationError(f"Could not parse constraint: {raw_constraint}")

@available
@classmethod
def render_raw_column_constraint(cls, raw_constraint: Dict[str, Any]) -> str:
constraint = cls._parse_column_constraint(raw_constraint)
return cls.render_column_constraint(constraint)

@classmethod
def render_column_constraint(cls, constraint: ColumnLevelConstraint) -> str:
def render_column_constraint(cls, constraint: ColumnLevelConstraint) -> Optional[str]:
"""Render the given constraint as DDL text. Should be overriden by adapters which need custom constraint
rendering."""
if constraint.type == ConstraintType.check and constraint.expression:
Expand All @@ -1296,7 +1308,46 @@ def render_column_constraint(cls, constraint: ColumnLevelConstraint) -> str:
elif constraint.type == ConstraintType.custom and constraint.expression:
return constraint.expression
else:
return ""
return None

@available
@classmethod
def render_raw_columns_constraints(cls, raw_columns: Dict[str, Dict[str, Any]]) -> List:
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm so selfishly delighted to see this now as a Python method and the types for this call stack standardized 🥂

rendered_column_constraints = []

for v in raw_columns.values():
rendered_column_constraint = [f"{v['name']} {v['data_type']}"]
for con in v.get("constraints", None):
constraint = cls._parse_column_constraint(con)
c = cls.process_parsed_constraint(constraint, cls.render_column_constraint)
if c is not None:
rendered_column_constraint.append(c)
rendered_column_constraints.append(" ".join(rendered_column_constraint))

return rendered_column_constraints

@classmethod
def process_parsed_constraint(
cls, parsed_constraint: Union[ColumnLevelConstraint, ModelLevelConstraint], render_func
) -> Optional[str]:
if (
parsed_constraint.warn_unsupported
and cls.CONSTRAINT_SUPPORT[parsed_constraint.type] == ConstraintSupport.NOT_SUPPORTED
):
warn_or_error(
ConstraintNotSupported(constraint=parsed_constraint.type.value, adapter=cls.type())
)
if (
parsed_constraint.warn_unenforced
and cls.CONSTRAINT_SUPPORT[parsed_constraint.type] == ConstraintSupport.NOT_ENFORCED
):
warn_or_error(
ConstraintNotEnforced(constraint=parsed_constraint.type.value, adapter=cls.type())
)
if cls.CONSTRAINT_SUPPORT[parsed_constraint.type] != ConstraintSupport.NOT_SUPPORTED:
return render_func(parsed_constraint)

return None

@classmethod
def _parse_model_constraint(cls, raw_constraint: Dict[str, Any]) -> ModelLevelConstraint:
Expand All @@ -1315,7 +1366,7 @@ def render_raw_model_constraints(cls, raw_constraints: List[Dict[str, Any]]) ->
@classmethod
def render_raw_model_constraint(cls, raw_constraint: Dict[str, Any]) -> Optional[str]:
constraint = cls._parse_model_constraint(raw_constraint)
return cls.render_model_constraint(constraint)
return cls.process_parsed_constraint(constraint, cls.render_model_constraint)

@classmethod
def render_model_constraint(cls, constraint: ModelLevelConstraint) -> Optional[str]:
Expand Down
22 changes: 22 additions & 0 deletions core/dbt/events/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,28 @@ message FinishedRunningStatsMsg {
FinishedRunningStats data = 2;
}

// E048
message ConstraintNotEnforced {
string constraint = 1;
string adapter = 2;
}

message ConstraintNotEnforcedMsg {
EventInfo info = 1;
ConstraintNotEnforced data = 2;
}

// E049
message ConstraintNotSupported {
string constraint = 1;
string adapter = 2;
}

message ConstraintNotSupportedMsg {
EventInfo info = 1;
ConstraintNotSupported data = 2;
}

// I - Project parsing

// I001
Expand Down
26 changes: 26 additions & 0 deletions core/dbt/events/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,32 @@ def message(self) -> str:
return f"Finished running {self.stat_line}{self.execution} ({self.execution_time:0.2f}s)."


class ConstraintNotEnforced(WarnLevel):
def code(self):
return "E048"

def message(self) -> str:
msg = (
f"The constraint type {self.constraint} is not enforced by {self.adapter}. "
"The constraint will be included in this model's DDL statement, but it will not "
"guarantee anything about the underlying data. Set 'warn_unenforced: false' on "
"this constraint to ignore this warning."
)
return line_wrap_message(warning_tag(msg))


class ConstraintNotSupported(WarnLevel):
def code(self):
return "E049"

def message(self) -> str:
msg = (
f"The constraint type {self.constraint} is not supported by {self.adapter}, and will "
"be ignored. Set 'warn_unsupported: false' on this constraint to ignore this warning."
)
return line_wrap_message(warning_tag(msg))


# =======================================================
# I - Project parsing
# =======================================================
Expand Down
1,110 changes: 559 additions & 551 deletions core/dbt/events/types_pb2.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,13 @@

{% macro table_columns_and_constraints() %}
{# loop through user_provided_columns to create DDL with data types and constraints #}
{%- set user_provided_columns = model['columns'] -%}
{%- set raw_model_constraints = adapter.render_raw_model_constraints(model['constraints']) -%}
{%- set raw_column_constraints = adapter.render_raw_columns_constraints(raw_columns=model['columns']) -%}
{%- set raw_model_constraints = adapter.render_raw_model_constraints(raw_constraints=model['constraints']) -%}
(
{% for i in user_provided_columns %}
{%- set col = user_provided_columns[i] -%}
{%- set constraints = col['constraints'] -%}
{{ col['name'] }} {{ col['data_type'] }}
{%- for c in constraints -%}
{%- set constraint_str = adapter.render_raw_column_constraint(c) -%}
{%- if constraint_str -%}
{{ ' ' ~ constraint_str }}
{%- endif -%}
{%- endfor -%}{{ "," if not loop.last or raw_model_constraints }}
{% endfor -%}
{% for c in raw_model_constraints %}
{% for c in raw_column_constraints -%}
{{ c }}{{ "," if not loop.last or raw_model_constraints }}
{% endfor %}
{% for c in raw_model_constraints -%}
{{ c }}{{ "," if not loop.last }}
{% endfor -%}
)
Expand Down
11 changes: 10 additions & 1 deletion plugins/postgres/dbt/adapters/postgres/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from dataclasses import dataclass
from typing import Optional, Set, List, Any
from dbt.adapters.base.meta import available
from dbt.adapters.base.impl import AdapterConfig
from dbt.adapters.base.impl import AdapterConfig, ConstraintSupport
from dbt.adapters.sql import SQLAdapter
from dbt.adapters.postgres import PostgresConnectionManager
from dbt.adapters.postgres.column import PostgresColumn
from dbt.adapters.postgres import PostgresRelation
from dbt.dataclass_schema import dbtClassMixin, ValidationError
from dbt.contracts.graph.nodes import ConstraintType
from dbt.exceptions import (
CrossDbReferenceProhibitedError,
IndexConfigNotDictError,
Expand Down Expand Up @@ -64,6 +65,14 @@ class PostgresAdapter(SQLAdapter):

AdapterSpecificConfigs = PostgresConfig

CONSTRAINT_SUPPORT = {
ConstraintType.check: ConstraintSupport.ENFORCED,
ConstraintType.not_null: ConstraintSupport.ENFORCED,
ConstraintType.unique: ConstraintSupport.ENFORCED,
ConstraintType.primary_key: ConstraintSupport.ENFORCED,
ConstraintType.foreign_key: ConstraintSupport.ENFORCED,
}

@classmethod
def date_function(cls):
return "now()"
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/artifacts/expected_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,8 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False):
"constraints": [],
},
},
"constraints": [],
"contract": {"checksum": None, "enforced": False},
"constraints": [],
"patch_path": "test://" + model_schema_yml_path,
"docs": {"node_color": None, "show": False},
"compiled": True,
Expand Down Expand Up @@ -412,8 +412,8 @@ def expected_seeded_manifest(project, model_database=None, quote_model=False):
"constraints": [],
},
},
"constraints": [],
"contract": {"checksum": None, "enforced": False},
"constraints": [],
"patch_path": "test://" + model_schema_yml_path,
"docs": {"node_color": None, "show": False},
"compiled": True,
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ def test_event_codes(self):
types.DatabaseErrorRunningHook(hook_type=""),
types.HooksRunning(num_hooks=0, hook_type=""),
types.FinishedRunningStats(stat_line="", execution="", execution_time=0),
types.ConstraintNotEnforced(constraint="", adapter=""),
types.ConstraintNotSupported(constraint="", adapter=""),
# I - Project parsing ======================
types.InputFileDiffError(category="testing", file_id="my_file"),
types.InvalidValueForField(field_name="test", field_value="test"),
Expand Down