Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADAP-869: Support atomic replace in replace macro #8539

Merged
merged 46 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f0b07d0
first draft of adding in table - materialized view swap
mikealfare Aug 17, 2023
24d9c42
Merge branch 'main' into feature/materialized-views/adap-814
mikealfare Aug 17, 2023
7372440
tests pass
mikealfare Aug 17, 2023
d20f7dc
table/view/materialized view can all replace each other
mikealfare Aug 17, 2023
28e1612
changie
mikealfare Aug 17, 2023
4483f6c
update renameable relations to a config
mikealfare Aug 17, 2023
03a15e7
revert to just reformatting changes
mikealfare Aug 18, 2023
d755c51
revert to just reformatting changes
mikealfare Aug 18, 2023
d898469
revert to just reformatting changes
mikealfare Aug 18, 2023
46e8aef
revert to just reformatting changes
mikealfare Aug 19, 2023
3572593
Revert "revert to just reformatting changes"
mikealfare Aug 19, 2023
4404b85
Revert "revert to just reformatting changes"
mikealfare Aug 19, 2023
e7909aa
Revert "revert to just reformatting changes"
mikealfare Aug 19, 2023
71cc6ed
Revert "revert to just reformatting changes"
mikealfare Aug 19, 2023
5dc4aea
Merge branch 'main' into feature/materialized-views/adap-814
mikealfare Aug 21, 2023
38bbbdb
migrate relations macros from `macros/adapters/relations` to `macros/…
mikealfare Aug 22, 2023
a6c5c0e
update comment to be more clear
mikealfare Aug 23, 2023
6dbaabb
loosen type on relations_that_can_be_renamed to StrEnum to account fo…
mikealfare Aug 23, 2023
aaec703
move drop macro to drop macro file
mikealfare Aug 25, 2023
6159fd4
remove view and table from generic create template, it's not needed a…
mikealfare Aug 28, 2023
99a1bb4
align the behavior of get_drop_sql and drop_relation, adopt existing …
mikealfare Aug 28, 2023
17aae86
whitespace/readability
mikealfare Aug 28, 2023
a31189e
add explicit ddl for drop statements instead of inheriting the defaul…
mikealfare Aug 29, 2023
24ebc37
update renameable relations attribute on BaseRelation
mikealfare Aug 31, 2023
a7db607
update replace macro dependent macros to align with naming standards
mikealfare Aug 31, 2023
8b643bd
update type for mashumaro, update related test
mikealfare Aug 31, 2023
f513194
move create and replace macros into their own files
mikealfare Aug 31, 2023
2f6cc57
add way to register replaceable relation types
mikealfare Aug 31, 2023
9a9c163
add interface for applying create or replace
mikealfare Aug 31, 2023
37cbab8
move config changes into alter.sql in alignment with other adapters
mikealfare Aug 31, 2023
30ab6b5
move config changes into alter.sql in alignment with other adapters
mikealfare Aug 31, 2023
05c1e61
move config changes into alter.sql in alignment with other adapters
mikealfare Aug 31, 2023
10f9c99
move shared relations macros to relations root
mikealfare Aug 31, 2023
8e6930d
move single file to models root
mikealfare Aug 31, 2023
eaee53e
add table to replace
mikealfare Aug 31, 2023
59e042c
move create file into relation directory
mikealfare Aug 31, 2023
0427da8
add table to replace
mikealfare Aug 31, 2023
3f1bfcc
implement replace for postgres
mikealfare Aug 31, 2023
10d046d
move column specific macros into column directory
mikealfare Aug 31, 2023
94edba7
Merge branch 'main' into feature/clone/replace
mikealfare Sep 1, 2023
81098e8
changie
mikealfare Sep 1, 2023
922235b
remove empty file
mikealfare Sep 1, 2023
de446b6
add unit test for can be replaced
mikealfare Sep 1, 2023
9d2b521
fixed defaults for renameable_relations and replaceable_relations
mikealfare Sep 1, 2023
957a21d
fixed tests for new defaults
mikealfare Sep 1, 2023
882aac9
update renameable_relations and replaceable_relations to frozensets t…
mikealfare Sep 1, 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/Features-20230831-204804.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Features
body: Support atomic replace in the global replace macro
time: 2023-08-31T20:48:04.098933-04:00
custom:
Author: mikealfare
Issue: "8539"
14 changes: 9 additions & 5 deletions core/dbt/adapters/base/relation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections.abc import Hashable
from dataclasses import dataclass, field
from typing import Optional, TypeVar, Any, Type, Dict, Iterator, Tuple, Set, List
from typing import Optional, TypeVar, Any, Type, Dict, Iterator, Tuple, Set, FrozenSet

from dbt.contracts.graph.nodes import SourceDefinition, ManifestNode, ResultNode, ParsedNode
from dbt.contracts.relation import (
Expand Down Expand Up @@ -36,9 +36,9 @@ class BaseRelation(FakeAPIObject, Hashable):
quote_policy: Policy = field(default_factory=lambda: Policy())
dbt_created: bool = False
# register relation types that can be renamed for the purpose of replacing relations using stages and backups
renameable_relations: List[str] = field(
default_factory=lambda: [RelationType.Table, RelationType.View]
)
renameable_relations: FrozenSet[str] = frozenset()
# register relation types that are replaceable, i.e. they have "create or replace" capability
replaceable_relations: FrozenSet[str] = frozenset()

def _is_exactish_match(self, field: ComponentName, value: str) -> bool:
if self.dbt_created and self.quote_policy.get_part(field) is False:
Expand Down Expand Up @@ -290,9 +290,13 @@ def create(
return cls.from_dict(kwargs)

@property
def can_be_renamed(self):
def can_be_renamed(self) -> bool:
return self.type in self.renameable_relations

@property
def can_be_replaced(self) -> bool:
return self.type in self.replaceable_relations

def __repr__(self) -> str:
return "<{} {}>".format(self.__class__.__name__, self.render())

Expand Down
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to relations/materialized_view/alter.sql

This file was deleted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Contents were moved to relations/view/replace.sql.

This file was deleted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced with relations/materialized_view/replace.sql, should have shown as a rename. That file was then updated to what shows below.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,28 @@
) %}
{{ exceptions.raise_compiler_error("Materialized views have not been implemented for this adapter.") }}
{% endmacro %}


{% macro get_materialized_view_configuration_changes(existing_relation, new_config) %}
/* {#
It's recommended that configuration changes be formatted as follows:
{"<change_category>": [{"action": "<name>", "context": ...}]}

For example:
{
"indexes": [
{"action": "drop", "context": "index_abc"},
{"action": "create", "context": {"columns": ["column_1", "column_2"], "type": "hash", "unique": True}},
],
}

Either way, `get_materialized_view_configuration_changes` needs to align with `get_alter_materialized_view_as_sql`.
#} */
{{- log('Determining configuration changes on: ' ~ existing_relation) -}}
{%- do return(adapter.dispatch('get_materialized_view_configuration_changes', 'dbt')(existing_relation, new_config)) -%}
{% endmacro %}


{% macro default__get_materialized_view_configuration_changes(existing_relation, new_config) %}
{{ exceptions.raise_compiler_error("Materialized views have not been implemented for this adapter.") }}
{% endmacro %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% macro get_replace_materialized_view_sql(relation, sql) %}
{{- adapter.dispatch('get_replace_materialized_view_sql', 'dbt')(relation, sql) -}}
{% endmacro %}


{% macro default__get_replace_materialized_view_sql(relation, sql) %}
{{ exceptions.raise_compiler_error(
"`get_replace_materialized_view_sql` has not been implemented for this adapter."
) }}
{% endmacro %}
19 changes: 17 additions & 2 deletions core/dbt/include/global_project/macros/relations/replace.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,29 @@

{% macro default__get_replace_sql(existing_relation, target_relation, sql) %}

{# /* use a create or replace statement if possible */ #}

{% set is_replaceable = existing_relation.type == target_relation_type and existing_relation.is_replaceable %}

{% if is_replaceable and existing_relation.is_view %}
{{ get_replace_view_sql(target_relation, sql) }}

{% elif is_replaceable and existing_relation.is_table %}
{{ get_replace_table_sql(target_relation, sql) }}

{% elif is_replaceable and existing_relation.is_materialized_view %}
{{ get_replace_materialized_view_sql(target_relation, sql) }}

{# /* a create or replace statement is not possible, so try to stage and/or backup to be safe */ #}

{# /* create target_relation as an intermediate relation, then swap it out with the existing one using a backup */ #}
{%- if target_relation.can_be_renamed and existing_relation.can_be_renamed -%}
{%- elif target_relation.can_be_renamed and existing_relation.can_be_renamed -%}
{{ get_create_intermediate_sql(target_relation, sql) }};
{{ get_create_backup_sql(existing_relation) }};
{{ get_rename_intermediate_sql(target_relation) }};
{{ get_drop_backup_sql(existing_relation) }}

{# /* create target_relation as an intermediate relation, then swap it out with the existing one using drop */ #}
{# /* create target_relation as an intermediate relation, then swap it out with the existing one without using a backup */ #}
{%- elif target_relation.can_be_renamed -%}
{{ get_create_intermediate_sql(target_relation, sql) }};
{{ get_drop_sql(existing_relation) }};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% macro get_replace_table_sql(relation, sql) %}
{{- adapter.dispatch('get_replace_table_sql', 'dbt')(relation, sql) -}}
{% endmacro %}


{% macro default__get_replace_table_sql(relation, sql) %}
{{ exceptions.raise_compiler_error(
"`get_replace_table_sql` has not been implemented for this adapter."
) }}
{% endmacro %}
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
{% macro get_replace_view_sql(relation, sql) %}
{{- adapter.dispatch('get_replace_view_sql', 'dbt')(relation, sql) -}}
{% endmacro %}


{% macro default__get_replace_view_sql(relation, sql) %}
{{ exceptions.raise_compiler_error(
"`get_replace_view_sql` has not been implemented for this adapter."
) }}
{% endmacro %}


/* {#
Core materialization implementation. BigQuery and Snowflake are similar
because both can use `create or replace view` where the resulting view's columns
Expand Down Expand Up @@ -42,3 +54,13 @@
{{ return({'relations': [target_relation]}) }}

{% endmacro %}


{% macro handle_existing_table(full_refresh, old_relation) %}
{{ adapter.dispatch('handle_existing_table', 'dbt')(full_refresh, old_relation) }}
{% endmacro %}

{% macro default__handle_existing_table(full_refresh, old_relation) %}
{{ log("Dropping relation " ~ old_relation ~ " because it is of type " ~ old_relation.type) }}
{{ adapter.drop_relation(old_relation) }}
{% endmacro %}
18 changes: 13 additions & 5 deletions plugins/postgres/dbt/adapters/postgres/relation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@

@dataclass(frozen=True, eq=False, repr=False)
class PostgresRelation(BaseRelation):
relations_that_can_be_renamed = [
RelationType.View,
RelationType.Table,
RelationType.MaterializedView,
]
renameable_relations = frozenset(
{
RelationType.View,
RelationType.Table,
RelationType.MaterializedView,
}
)
replaceable_relations = frozenset(
{
RelationType.View,
RelationType.Table,
}
)

def __post_init__(self):
# Check for length of Postgres table/view names.
Expand Down
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Contents of this file were moved to relations/materialized_view/alter.sql.

This file was deleted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced with relations/materialized_view/replace.sql, should have shown as a rename. The contents of that file were then updated to the below.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,10 @@
{%- endfor -%}

{%- endmacro -%}


{% macro postgres__get_materialized_view_configuration_changes(existing_relation, new_config) %}
{% set _existing_materialized_view = postgres__describe_materialized_view(existing_relation) %}
{% set _configuration_changes = existing_relation.get_materialized_view_config_change_collection(_existing_materialized_view, new_config) %}
{% do return(_configuration_changes) %}
{% endmacro %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% macro postgres__get_replace_table_sql(relation, sql) -%}

{%- set sql_header = config.get('sql_header', none) -%}
{{ sql_header if sql_header is not none }}

create or replace table {{ relation }}
{% set contract_config = config.get('contract') %}
{% if contract_config.enforced %}
{{ get_assert_columns_equivalent(sql) }}
{{ get_table_columns_and_constraints() }}
{%- set sql = get_select_subquery(sql) %}
{% endif %}
as (
{{ sql }}
);

{%- endmacro %}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was taken from the default create view statement in dbt-core. Postgres did not overwrite it. It was then updated to run a create or replace instead of just replace and whitespace was adjusted for readability; otherwise it was left untouched.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% macro postgres__get_replace_view_sql(relation, sql) -%}

{%- set sql_header = config.get('sql_header', none) -%}
{{ sql_header if sql_header is not none }}

create or replace view {{ relation }}
{% set contract_config = config.get('contract') %}
{% if contract_config.enforced %}
{{ get_assert_columns_equivalent(sql) }}
{%- endif %}
as (
{{ sql }}
);

{%- endmacro %}
26 changes: 26 additions & 0 deletions tests/unit/test_relation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from dataclasses import replace

import pytest

from dbt.adapters.base import BaseRelation
Expand All @@ -13,4 +15,28 @@
)
def test_can_be_renamed(relation_type, result):
my_relation = BaseRelation.create(type=relation_type)
my_relation = replace(my_relation, renameable_relations=frozenset({RelationType.View}))
assert my_relation.can_be_renamed is result


def test_can_be_renamed_default():
my_relation = BaseRelation.create(type=RelationType.View)
assert my_relation.can_be_renamed is False


@pytest.mark.parametrize(
"relation_type,result",
[
(RelationType.View, True),
(RelationType.External, False),
],
)
def test_can_be_replaced(relation_type, result):
my_relation = BaseRelation.create(type=relation_type)
my_relation = replace(my_relation, replaceable_relations=frozenset({RelationType.View}))
assert my_relation.can_be_replaced is result


def test_can_be_replaced_default():
my_relation = BaseRelation.create(type=RelationType.View)
assert my_relation.can_be_replaced is False