Skip to content
Closed
Show file tree
Hide file tree
Changes from 118 commits
Commits
Show all changes
119 commits
Select commit Hold shift + click to select a range
e879cdd
start off the blueprints
sungchun12 Nov 7, 2022
bd1afc7
Merge branch 'main' of https://github.com/dbt-labs/dbt into feature/d…
sungchun12 Nov 9, 2022
4b2a881
Merge branch 'main' of https://github.com/dbt-labs/dbt into feature/d…
sungchun12 Nov 16, 2022
e8b52e0
test commit
sungchun12 Nov 16, 2022
01a07d3
working snowflake env
sungchun12 Nov 16, 2022
3b31a15
update manifest expectation
sungchun12 Nov 17, 2022
1a1f46a
add error handling
sungchun12 Nov 17, 2022
ebaa54c
clean up language
sungchun12 Nov 17, 2022
fd7a47a
constraints validator
sungchun12 Nov 17, 2022
7e3a9be
cleaner example
sungchun12 Nov 17, 2022
6df02da
better terminal output
sungchun12 Nov 17, 2022
bc3c5bc
add python error handling
sungchun12 Nov 17, 2022
4b901fd
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Nov 17, 2022
e29b571
add to manifest schema
sungchun12 Nov 17, 2022
7d085dc
add to schema config
sungchun12 Nov 17, 2022
e6559d4
clean up comments
sungchun12 Nov 18, 2022
ca89141
backwards compatible nodeconfig
sungchun12 Nov 18, 2022
2c4a4cf
remove comments
sungchun12 Nov 18, 2022
1975c6b
clean up more comments
sungchun12 Nov 18, 2022
f088a03
add changelog
sungchun12 Nov 18, 2022
7421caa
clarify error message
sungchun12 Nov 21, 2022
a7395bb
constraints list type
sungchun12 Nov 21, 2022
5d06524
fix grammar
sungchun12 Nov 21, 2022
380bd96
add global macros
sungchun12 Nov 21, 2022
e6e490d
clearer compile error
sungchun12 Nov 21, 2022
9c498ef
remove comments
sungchun12 Nov 21, 2022
bed1fec
fix tests in this file
sungchun12 Nov 21, 2022
8c466b0
conditional compile errors
sungchun12 Nov 22, 2022
547ad9e
add conditional check in ddl
sungchun12 Nov 22, 2022
52bd35b
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Nov 22, 2022
7582531
add macro to dispatch
sungchun12 Nov 22, 2022
7291094
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Nov 28, 2022
b87f57d
fix regressions in parsed
sungchun12 Nov 28, 2022
5529334
fix regressions in manifest tests
sungchun12 Nov 28, 2022
00f12c2
fix manifest test regressions
sungchun12 Nov 28, 2022
76bf69c
fix test_list regressions
sungchun12 Nov 28, 2022
2e51246
concise data_type terminal error
sungchun12 Nov 29, 2022
5891eb3
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Nov 29, 2022
5d2867f
remove placeholder function
sungchun12 Nov 29, 2022
801b2fd
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Nov 29, 2022
5d59cc1
fix failed regressions finally
sungchun12 Dec 2, 2022
92d2ea7
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Dec 2, 2022
4f747b0
Revert "Merge branch 'main' of https://github.com/dbt-labs/dbt into d…
sungchun12 Dec 2, 2022
eba0b6d
Revert "Revert "Merge branch 'main' of https://github.com/dbt-labs/db…
sungchun12 Dec 2, 2022
ae56da1
remove tmp.csv
sungchun12 Dec 2, 2022
de653e4
template test plans
sungchun12 Dec 5, 2022
cfc53b0
postgres columns spec macro
sungchun12 Dec 5, 2022
fc7230b
schema does not exist error handling
sungchun12 Dec 7, 2022
e1c72ac
update postgres adapter
sungchun12 Dec 7, 2022
b215b6c
remove comments
sungchun12 Dec 7, 2022
d550de4
first passing test
sungchun12 Dec 7, 2022
9d87463
fix postgres macro
sungchun12 Dec 7, 2022
ce85c96
add more passing tests
sungchun12 Dec 7, 2022
49a4120
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Dec 7, 2022
8ffb654
Add generated CLI API docs
FishtownBuildBot Dec 7, 2022
f2f2707
add disabled config test
sungchun12 Dec 7, 2022
096f3fd
column configs match
sungchun12 Dec 7, 2022
eae4e76
Merge branch 'dbt-constraints' of https://github.com/dbt-labs/dbt int…
sungchun12 Dec 7, 2022
b8c3812
test python error handling
sungchun12 Dec 7, 2022
bb1a6c3
adjust macro with rollback
sungchun12 Dec 7, 2022
b6dbcf6
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Dec 7, 2022
751cdc8
start postgres tests
sungchun12 Dec 8, 2022
6bbd797
remove begin commit
sungchun12 Dec 15, 2022
d364eeb
remove begin commit comments
sungchun12 Dec 15, 2022
4a58ece
passing expected compiled sql test
sungchun12 Dec 15, 2022
ac795dd
passing rollback test
sungchun12 Dec 15, 2022
d452cae
update changelog
sungchun12 Dec 16, 2022
baf18f0
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Dec 16, 2022
76c0e4f
fix artifacts regression
sungchun12 Dec 16, 2022
10ab3cb
modularize validator
sungchun12 Dec 18, 2022
6253ed0
PR feedback
sungchun12 Dec 18, 2022
307809d
verify database error occurs
sungchun12 Dec 19, 2022
ab4f396
focus on generic outcomes
sungchun12 Dec 19, 2022
b99e9be
fix global macro
sungchun12 Dec 20, 2022
5935201
rename to constraints_check
sungchun12 Dec 20, 2022
f59c9dd
missed a check rename
sungchun12 Dec 20, 2022
7e28a31
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Dec 24, 2022
3ddd666
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Jan 4, 2023
fabe2ce
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Jan 4, 2023
ffec7d7
validate at parse time
sungchun12 Jan 4, 2023
d338f33
raise error for modelparser only
sungchun12 Jan 5, 2023
e34c467
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Jan 5, 2023
44b2f18
better spacing in terminal output
sungchun12 Jan 5, 2023
17b1f8e
fix test regressions
sungchun12 Jan 6, 2023
c652367
fix manifest test regressions
sungchun12 Jan 6, 2023
f9e020d
these are parsing errors now
sungchun12 Jan 9, 2023
926e555
merge main
sungchun12 Jan 11, 2023
c6bd674
fix tests
sungchun12 Jan 11, 2023
bcc35fc
test passes in json log format
sungchun12 Jan 11, 2023
880ed43
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Jan 13, 2023
426789e
add column compile error handling
sungchun12 Jan 13, 2023
3d61eda
update global macros for column handling
sungchun12 Jan 13, 2023
f163b2c
remove TODO
sungchun12 Jan 13, 2023
bf45243
uppercase columns for consistency
sungchun12 Jan 17, 2023
dbef42b
more specific error handling
sungchun12 Jan 17, 2023
c4de8f3
migrate tests
sungchun12 Jan 17, 2023
ad07ced
clean up core tests
sungchun12 Jan 17, 2023
a501c29
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Jan 17, 2023
59b0298
Update core/dbt/include/global_project/macros/materializations/models…
sungchun12 Jan 17, 2023
e46066b
Revert "Update core/dbt/include/global_project/macros/materialization…
sungchun12 Jan 17, 2023
e80b8cd
update for pre-commit hooks
sungchun12 Jan 17, 2023
257eacd
update for black formatter
sungchun12 Jan 17, 2023
391bd3b
update for black formatter on all files
sungchun12 Jan 17, 2023
0441417
Merge remote-tracking branch 'origin/main' into dbt-constraints
jtcohen6 Jan 23, 2023
e0bcb25
Refactor functional tests
jtcohen6 Jan 30, 2023
dcf7062
Fixup formatting
jtcohen6 Jan 30, 2023
903a2cb
Dave feedback
jtcohen6 Jan 30, 2023
c40ee92
another one - dave
jtcohen6 Jan 30, 2023
111683a
the hits keep coming
jtcohen6 Jan 30, 2023
f8b16bc
adjust whitespace
dave-connors-3 Jan 30, 2023
97f0c6b
Merge branch 'main' of https://github.com/dbt-labs/dbt into dbt-const…
sungchun12 Jan 31, 2023
2a33baf
Light touchup
jtcohen6 Feb 1, 2023
6806a7c
Merge remote-tracking branch 'origin/main' into dbt-constraints
jtcohen6 Feb 1, 2023
1256e7b
Add more flexibility for spark
jtcohen6 Feb 2, 2023
0304dbf
Nearly there for spark
jtcohen6 Feb 2, 2023
b5b1699
Merge main
jtcohen6 Feb 14, 2023
1b0c213
Experiment: column types from empty query
jtcohen6 Jan 20, 2023
64470ca
use get_empty_schema_sql to get column schema from schema yml
MichelleArk Feb 15, 2023
c3e6b7a
Merge branch 'main' into CT-1919/get_column_schema_from_query_macro_core
MichelleArk Feb 15, 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
8 changes: 8 additions & 0 deletions .changes/unreleased/Features-20221118-141120.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
kind: Features
body: Data type constraints are now native to SQL table materializations. Enforce
columns are specific data types and not null depending on database functionality.
time: 2022-11-18T14:11:20.868062-08:00
custom:
Author: sungchun12
Issue: "6079"
PR: "6271"
16 changes: 16 additions & 0 deletions core/dbt/adapters/base/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,22 @@ def execute(
"""
return self.connections.execute(sql=sql, auto_begin=auto_begin, fetch=fetch)

@available.parse(lambda *a, **k: [])
def get_column_schema_from_query(
self, sql: str, auto_begin: bool = False, fetch: bool = False
) -> Tuple[AdapterResponse, agate.Table]:
"""Execute the given SQL. This is a thin wrapper around
ConnectionManager.execute.

:param str sql: The sql to execute.
:param bool auto_begin: If set, and dbt is not currently inside a
transaction, automatically begin one.
:param bool fetch: If set, fetch results.
:return: A tuple of the query status and results (empty if fetch=False).
:rtype: List[(column_name: str, data_type: str]
"""
return self.connections.get_column_schema_from_query(sql=sql)

@available.parse(lambda *a, **k: ("", empty_table()))
def get_partitions_metadata(self, table: str) -> Tuple[agate.Table]:
"""Obtain partitions metadata for a BigQuery partitioned table.
Expand Down
39 changes: 39 additions & 0 deletions core/dbt/adapters/sql/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,31 @@ def get_result_from_cursor(cls, cursor: Any) -> agate.Table:

return dbt.clients.agate_helper.table_from_data_flat(data, column_names)

@classmethod
def data_type_code_to_name(cls, int) -> str:
"""Get the string representation of the data type from the type_code."""
# https://peps.python.org/pep-0249/#type-objects
raise dbt.exceptions.NotImplementedError(
"`data_type_code_to_name` is not implemented for this adapter!"
)

@classmethod
def get_column_schema_from_cursor(cls, cursor: Any) -> List[Tuple[str, str]]:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: change to -> List[Tuple[str, Any]]

# (column_name, data_type)
columns: List[Tuple[str, str]] = []

if cursor.description is not None:
# https://peps.python.org/pep-0249/#description
columns = [
# TODO: ignoring size, precision, scale for now
# (though it is part of DB-API standard, and our Column class does have these attributes)
# IMO user-defined contracts shouldn't have to match an exact size/precision/scale
(col[0], cls.data_type_code_to_name(col[1]))
for col in cursor.description
]

return columns

def execute(
self, sql: str, auto_begin: bool = False, fetch: bool = False
) -> Tuple[AdapterResponse, agate.Table]:
Expand All @@ -140,6 +165,20 @@ def execute(
table = dbt.clients.agate_helper.empty_table()
return response, table

# TODO: do we need to care about auto_begin here?
def get_column_schema_from_query(self, sql: str) -> List[Tuple[str, str]]:
sql = self._add_query_comment(sql)
_, cursor = self.add_query(sql)
return self.get_column_schema_from_cursor(cursor)

# For dbt-bigquery
# def get_column_schema_from_query(cls, sql: str) -> List[Tuple[str, str]]:
# sql = self._add_query_comment(sql)
# # auto_begin is ignored on bigquery, and only included for consistency
# query_job, iterator = self.raw_execute(sql)
# columns = [(field.name, field.field_type) for field in resp.iterator]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

s/resp.iterator/iterator.schema

# return columns

def add_begin_query(self):
return self.add_query("BEGIN", auto_begin=False)

Expand Down
1 change: 1 addition & 0 deletions core/dbt/contracts/graph/model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ class NodeConfig(NodeAndTestConfig):
default_factory=Docs,
metadata=MergeBehavior.Update.meta(),
)
constraints_enabled: Optional[bool] = False

# we validate that node_color has a suitable value to prevent dbt-docs from crashing
def __post_init__(self):
Expand Down
4 changes: 4 additions & 0 deletions core/dbt/contracts/graph/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
SnapshotConfig,
)


# =====================================================================
# This contains the classes for all of the nodes and node-like objects
# in the manifest. In the "nodes" dictionary of the manifest we find
Expand Down Expand Up @@ -146,6 +147,8 @@ class ColumnInfo(AdditionalPropertiesMixin, ExtensibleDbtClassMixin, Replaceable
description: str = ""
meta: Dict[str, Any] = field(default_factory=dict)
data_type: Optional[str] = None
constraints: Optional[List[str]] = None
constraints_check: Optional[str] = None
quote: Optional[bool] = None
tags: List[str] = field(default_factory=list)
_extra: Dict[str, Any] = field(default_factory=dict)
Expand Down Expand Up @@ -400,6 +403,7 @@ class CompiledNode(ParsedNode):
extra_ctes_injected: bool = False
extra_ctes: List[InjectedCTE] = field(default_factory=list)
_pre_injected_sql: Optional[str] = None
constraints_enabled: bool = False

@property
def empty(self):
Expand Down
2 changes: 2 additions & 0 deletions core/dbt/contracts/graph/unparsed.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class HasDocs(AdditionalPropertiesMixin, ExtensibleDbtClassMixin, Replaceable):
description: str = ""
meta: Dict[str, Any] = field(default_factory=dict)
data_type: Optional[str] = None
constraints: Optional[List[str]] = None
constraints_check: Optional[str] = None
docs: Docs = field(default_factory=Docs)
_extra: Dict[str, Any] = field(default_factory=dict)

Expand Down
Binary file removed core/dbt/docs/build/doctrees/environment.pickle
Binary file not shown.
51 changes: 44 additions & 7 deletions core/dbt/include/global_project/macros/adapters/columns.sql
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,60 @@
{% endmacro %}


{% macro get_empty_subquery_sql(select_sql) -%}
{{ return(adapter.dispatch('get_empty_subquery_sql', 'dbt')(select_sql)) }}
{% endmacro %}

{% macro default__get_empty_subquery_sql(select_sql) %}
select * from (
{{ select_sql }}
) as __dbt_sbq
where false
limit 0
{% endmacro %}


{% macro get_empty_schema_sql(columns) -%}
{{ return(adapter.dispatch('get_empty_schema_sql', 'dbt')(columns)) }}
{% endmacro %}

{% macro default__get_empty_schema_sql(columns) %}
select
{% for i in columns %}
{%- set col = columns[i] -%}
cast(null as {{ col['data_type'] }}) as {{ col['name'] }}{{ ", " if not loop.last }}
{%- endfor -%}
{% endmacro %}

{% macro get_column_schema_from_query(select_sql) -%}
{{ return(adapter.dispatch('get_column_schema_from_query', 'dbt')(select_sql)) }}
{% endmacro %}

{% macro default__get_column_schema_from_query(select_sql) %}
{% set columns = [] %}
{% set sql = get_empty_subquery_sql(select_sql) %}
{% set column_schema = adapter.get_column_schema_from_query(sql) %}
{% for col in column_schema %}
-- api.Column.create includes a step for translating data type
-- TODO: could include size, precision, scale here
{% set column = api.Column.create(col[0], col[1]) %}
{% do columns.append(column) %}
{% endfor %}
{{ return(columns) }}
{% endmacro %}

-- here for back compat
{% macro get_columns_in_query(select_sql) -%}
{{ return(adapter.dispatch('get_columns_in_query', 'dbt')(select_sql)) }}
{% endmacro %}

{% macro default__get_columns_in_query(select_sql) %}
{% call statement('get_columns_in_query', fetch_result=True, auto_begin=False) -%}
select * from (
{{ select_sql }}
) as __dbt_sbq
where false
limit 0
{{ get_empty_subquery_sql(select_sql) }}
{% endcall %}

{{ return(load_result('get_columns_in_query').table.columns | map(attribute='name') | list) }}
{% endmacro %}


{% macro alter_column_type(relation, column_name, new_column_type) -%}
{{ return(adapter.dispatch('alter_column_type', 'dbt')(relation, column_name, new_column_type)) }}
{% endmacro %}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{%- macro get_columns_spec_ddl() -%}
{{ adapter.dispatch('get_columns_spec_ddl', 'dbt')() }}
{%- endmacro -%}

{% macro default__get_columns_spec_ddl() -%}
{{ return(columns_spec_ddl()) }}
{%- endmacro %}

{% macro columns_spec_ddl() %}
{# loop through user_provided_columns to create DDL with data types and constraints #}
{%- set user_provided_columns = model['columns'] -%}
(
{% for i in user_provided_columns %}
{% set col = user_provided_columns[i] %}
{% set constraints = col['constraints'] %}
{% set constraints_check = col['constraints_check'] %}
{{ col['name'] }} {{ col['data_type'] }} {% for x in constraints %} {{ x or "" }} {% endfor %} {% if constraints_check -%} check {{ constraints_check or "" }} {%- endif %} {{ "," if not loop.last }}
{% endfor %}
)
{% endmacro %}

{%- macro get_assert_columns_equivalent(sql) -%}
{{ adapter.dispatch('get_assert_columns_equivalent', 'dbt')(sql) }}
{%- endmacro -%}

{% macro default__get_assert_columns_equivalent(sql) -%}
{{ return(assert_columns_equivalent(sql)) }}
{%- endmacro %}

{% macro assert_columns_equivalent(sql) %}
{%- set sql_file_provided_columns = get_column_schema_from_query(sql) -%}
{%- set schema_file_provided_columns = get_column_schema_from_query(get_empty_schema_sql(model['columns'])) -%}

{%- set sql_file_provided_columns_formatted = format_columns(sql_file_provided_columns) -%}
{%- set schema_file_provided_columns_formatted = format_columns(schema_file_provided_columns) -%}

{%- if sql_file_provided_columns_formatted != schema_file_provided_columns_formatted -%}
{%- do exceptions.raise_compiler_error('Please ensure the name, data_type, order, and number of columns in your `yml` file match the columns in your SQL file.\nSchema File Columns: ' ~ sql_file_provided_columns_formatted ~ '\n\nSQL File Columns: ' ~ schema_file_provided_columns_formatted ~ ' ' ) %}
{%- endif -%}

{% endmacro %}

{% macro format_columns(columns) %}
{{ adapter.dispatch('format_columns', 'dbt')(columns) }}
{%- endmacro %}

{% macro default__format_columns(columns) -%}
{% set formatted_columns = [] %}
{% for column in columns %}
{%- set formatted_column = column.column.lower() ~ " " ~ column.dtype -%}
{%- do formatted_columns.append(formatted_column) -%}
{% endfor %}
{{ return(formatted_columns|join(', ')) }}
{%- endmacro -%}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@

create {% if temporary: -%}temporary{%- endif %} table
{{ relation.include(database=(not temporary), schema=(not temporary)) }}
{% if config.get('constraints_enabled', False) %}
{{ get_assert_columns_equivalent(sql) }}
{{ get_columns_spec_ddl() }}
{% endif %}
as (
{{ sql }}
);
Expand Down
15 changes: 14 additions & 1 deletion core/dbt/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.graph.nodes import ManifestNode, BaseNode
from dbt.contracts.graph.unparsed import UnparsedNode, Docs
from dbt.exceptions import DbtInternalError, ConfigUpdateError, DictParseError
from dbt.exceptions import DbtInternalError, ConfigUpdateError, DictParseError, ParsingError
from dbt import hooks
from dbt.node_types import NodeType, ModelLanguage
from dbt.parser.search import FileBlock
Expand Down Expand Up @@ -306,6 +306,19 @@ def update_parsed_node_config(
else:
parsed_node.docs = Docs(show=docs_show)

# If we have constraints_enabled in the config, copy to node level, for backwards
# compatibility with earlier node-only config.
if config_dict.get("constraints_enabled", False):
parsed_node.constraints_enabled = True

parser_name = type(self).__name__
if parser_name == "ModelParser":
original_file_path = parsed_node.original_file_path
error_message = "\n `constraints_enabled=true` can only be configured within `schema.yml` files\n NOT within a model file(ex: .sql, .py) or `dbt_project.yml`."
raise ParsingError(
f"Original File Path: ({original_file_path})\nConstraints must be defined in a `yml` schema configuration file like `schema.yml`.\nOnly the SQL table materialization is supported for constraints. \n`data_type` values must be defined for all columns and NOT be null or blank.{error_message}"
)

# unrendered_config is used to compare the original database/schema/alias
# values and to handle 'same_config' and 'same_contents' calls
parsed_node.unrendered_config = config.build_config_dict(
Expand Down
77 changes: 76 additions & 1 deletion core/dbt/parser/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ def add(
column: Union[HasDocs, UnparsedColumn],
description: str,
data_type: Optional[str],
constraints: Optional[List[str]],
constraints_check: Optional[str],
meta: Dict[str, Any],
):
tags: List[str] = []
Expand All @@ -132,6 +134,8 @@ def add(
name=column.name,
description=description,
data_type=data_type,
constraints=constraints,
constraints_check=constraints_check,
meta=meta,
tags=tags,
quote=quote,
Expand All @@ -144,8 +148,10 @@ def from_target(cls, target: Union[HasColumnDocs, HasColumnTests]) -> "ParserRef
for column in target.columns:
description = column.description
data_type = column.data_type
constraints = column.constraints
constraints_check = column.constraints_check
meta = column.meta
refs.add(column, description, data_type, meta)
refs.add(column, description, data_type, constraints, constraints_check, meta)
return refs


Expand Down Expand Up @@ -914,6 +920,75 @@ def parse_patch(self, block: TargetBlock[NodeTarget], refs: ParserRef) -> None:
self.patch_node_config(node, patch)

node.patch(patch)
self.validate_constraints(node)

def validate_constraints(self, patched_node):
error_messages = []
if (
patched_node.resource_type == "model"
and patched_node.config.constraints_enabled is True
):
validators = [
self.constraints_schema_validator(patched_node),
self.constraints_materialization_validator(patched_node),
self.constraints_language_validator(patched_node),
self.constraints_data_type_validator(patched_node),
]
error_messages = [validator for validator in validators if validator != "None"]

if error_messages:
original_file_path = patched_node.original_file_path
raise ParsingError(
f"Original File Path: ({original_file_path})\nConstraints must be defined in a `yml` schema configuration file like `schema.yml`.\nOnly the SQL table materialization is supported for constraints. \n`data_type` values must be defined for all columns and NOT be null or blank.{self.convert_errors_to_string(error_messages)}"
)

def convert_errors_to_string(self, error_messages: List[str]):
n = len(error_messages)
if not n:
return ""
if n == 1:
return error_messages[0]
error_messages_string = "".join(error_messages[:-1]) + f"{error_messages[-1]}"
return error_messages_string

def constraints_schema_validator(self, patched_node):
schema_error = False
if patched_node.columns == {}:
schema_error = True
schema_error_msg = "\n Schema Error: `yml` configuration does NOT exist"
schema_error_msg_payload = f"{schema_error_msg if schema_error else None}"
return schema_error_msg_payload

def constraints_materialization_validator(self, patched_node):
materialization_error = {}
if patched_node.config.materialized != "table":
materialization_error = {"materialization": patched_node.config.materialized}
materialization_error_msg = f"\n Materialization Error: {materialization_error}"
materialization_error_msg_payload = (
f"{materialization_error_msg if materialization_error else None}"
)
return materialization_error_msg_payload

def constraints_language_validator(self, patched_node):
language_error = {}
language = str(patched_node.language)
if language != "sql":
language_error = {"language": language}
language_error_msg = f"\n Language Error: {language_error}"
language_error_msg_payload = f"{language_error_msg if language_error else None}"
return language_error_msg_payload

def constraints_data_type_validator(self, patched_node):
data_type_errors = set()
for column, column_info in patched_node.columns.items():
if column_info.data_type is None:
data_type_error = {column}
data_type_errors.update(data_type_error)
data_type_errors_msg = (
f"\n Columns with `data_type` Blank/Null Errors: {data_type_errors}"
)
data_type_errors_msg_payload = f"{data_type_errors_msg if data_type_errors else None}"
return data_type_errors_msg_payload


class TestablePatchParser(NodePatchParser[UnparsedNodeUpdate]):
Expand Down
Loading