From 7ba40c92385debf7b300f013c7b83d62338c71ef Mon Sep 17 00:00:00 2001 From: HenkvanDyk Date: Tue, 30 Jan 2024 16:02:02 +0000 Subject: [PATCH 01/13] feature: Update Dynamic table parameters on materialization --- .../snowflake/relation_configs/dynamic_table.py | 12 ++++++++++++ .../macros/relations/dynamic_table/create.sql | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/dbt/adapters/snowflake/relation_configs/dynamic_table.py b/dbt/adapters/snowflake/relation_configs/dynamic_table.py index cc1b9112d..ade9b6770 100644 --- a/dbt/adapters/snowflake/relation_configs/dynamic_table.py +++ b/dbt/adapters/snowflake/relation_configs/dynamic_table.py @@ -30,6 +30,9 @@ class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): query: str target_lag: str snowflake_warehouse: str + refresh_mode: str + initialize: str + comment: Optional[str] = None @classmethod def from_dict(cls, config_dict) -> "SnowflakeDynamicTableConfig": @@ -42,6 +45,9 @@ def from_dict(cls, config_dict) -> "SnowflakeDynamicTableConfig": "query": config_dict.get("query"), "target_lag": config_dict.get("target_lag"), "snowflake_warehouse": config_dict.get("snowflake_warehouse"), + "refresh_mode": config_dict.get("refresh_mode"), + "initialize": config_dict.get("initialize"), + "comment": config_dict.get("comment"), } dynamic_table: "SnowflakeDynamicTableConfig" = super().from_dict(kwargs_dict) # type: ignore @@ -56,6 +62,9 @@ def parse_relation_config(cls, relation_config: RelationConfig) -> Dict[str, Any "query": relation_config.compiled_code, # type: ignore "target_lag": relation_config.config.extra.get("target_lag"), # type: ignore "snowflake_warehouse": relation_config.config.extra.get("snowflake_warehouse"), # type: ignore + "refresh_mode": relation_config.config.extra.get("refresh_mode"), # type: ignore + "initialize": relation_config.config.extra.get("initialize"), # type: ignore + "comment": relation_config.config.extra.get("comment"), # type: ignore } return config_dict @@ -71,6 +80,9 @@ def parse_relation_results(cls, relation_results: RelationResults) -> Dict: "query": dynamic_table.get("text"), "target_lag": dynamic_table.get("target_lag"), "snowflake_warehouse": dynamic_table.get("warehouse"), + "refresh_mode": dynamic_table.get("refresh_mode"), # Should I set defaults here or do they get passed as null + "initialize": dynamic_table.get("initialize"), # Should I set defaults here or do they get passed as null + "comment": dynamic_table.get("comment"), } return config_dict diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/create.sql b/dbt/include/snowflake/macros/relations/dynamic_table/create.sql index 8e8f3287f..24ecc3977 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/create.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/create.sql @@ -2,7 +2,12 @@ create dynamic table {{ relation }} target_lag = '{{ config.get("target_lag") }}' + refresh_mode = '{{ config.get("refresh_mode") }}' + initialize = '{{ config.get("initialize") }}' warehouse = {{ config.get("snowflake_warehouse") }} + {% if config.get("comment") %} + comment = '{{ config.get("comment") }}' + {% endif %} as ( {{ sql }} ) From 17eb974b1b057c729ff9890342dd9fde237b98d0 Mon Sep 17 00:00:00 2001 From: HenkvanDyk Date: Wed, 31 Jan 2024 12:52:30 +0000 Subject: [PATCH 02/13] refactor: Update doc strings --- .../snowflake/relation_configs/dynamic_table.py | 11 +++++++---- .../macros/relations/dynamic_table/create.sql | 8 ++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/dbt/adapters/snowflake/relation_configs/dynamic_table.py b/dbt/adapters/snowflake/relation_configs/dynamic_table.py index ade9b6770..0db608736 100644 --- a/dbt/adapters/snowflake/relation_configs/dynamic_table.py +++ b/dbt/adapters/snowflake/relation_configs/dynamic_table.py @@ -20,6 +20,9 @@ class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): - query: the query behind the table - target_lag: the maximum amount of time that the dynamic table’s content should lag behind updates to the base tables - snowflake_warehouse: the name of the warehouse that provides the compute resources for refreshing the dynamic table + - refresh_mode: specifies the refresh type for the dynamic table. + - initialize: specifies the behavior of the initial refresh of the dynamic table + - comment: specifies a comment for the dynamic table. There are currently no non-configurable parameters. """ @@ -30,8 +33,8 @@ class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): query: str target_lag: str snowflake_warehouse: str - refresh_mode: str - initialize: str + refresh_mode: Optional[str] = None + initialize: Optional[str] = None comment: Optional[str] = None @classmethod @@ -80,8 +83,8 @@ def parse_relation_results(cls, relation_results: RelationResults) -> Dict: "query": dynamic_table.get("text"), "target_lag": dynamic_table.get("target_lag"), "snowflake_warehouse": dynamic_table.get("warehouse"), - "refresh_mode": dynamic_table.get("refresh_mode"), # Should I set defaults here or do they get passed as null - "initialize": dynamic_table.get("initialize"), # Should I set defaults here or do they get passed as null + "refresh_mode": dynamic_table.get("refresh_mode"), + "initialize": dynamic_table.get("initialize"), "comment": dynamic_table.get("comment"), } diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/create.sql b/dbt/include/snowflake/macros/relations/dynamic_table/create.sql index 24ecc3977..89e850ce9 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/create.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/create.sql @@ -2,9 +2,13 @@ create dynamic table {{ relation }} target_lag = '{{ config.get("target_lag") }}' - refresh_mode = '{{ config.get("refresh_mode") }}' - initialize = '{{ config.get("initialize") }}' warehouse = {{ config.get("snowflake_warehouse") }} + {% if config.get("refresh_mode") %} + refresh_mode = '{{ config.get("refresh_mode") }}' + {% endif %} + {% if config.get("initialize") %} + initialize = '{{ config.get("initialize") }}' + {% endif %} {% if config.get("comment") %} comment = '{{ config.get("comment") }}' {% endif %} From c92522eb41eb4ebc0c6f9f2255683ae5170a10d9 Mon Sep 17 00:00:00 2001 From: HenkvanDyk Date: Wed, 31 Jan 2024 12:53:30 +0000 Subject: [PATCH 03/13] feature: changelog --- .changes/unreleased/Features-20240131-125318.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/unreleased/Features-20240131-125318.yaml diff --git a/.changes/unreleased/Features-20240131-125318.yaml b/.changes/unreleased/Features-20240131-125318.yaml new file mode 100644 index 000000000..eee5c07d6 --- /dev/null +++ b/.changes/unreleased/Features-20240131-125318.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Dynamic Table additional parameters +time: 2024-01-31T12:53:18.111616Z +custom: + Author: HenkvanDyk + Issue: "1076" From 6076e3a005ce24fa8aea0276b20c4839c804b323 Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Tue, 11 Jun 2024 21:36:22 -0400 Subject: [PATCH 04/13] add new attributes to the describe dynamic table query --- .../snowflake/macros/relations/dynamic_table/describe.sql | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql b/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql index a5f612039..cc1553b9c 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql @@ -10,7 +10,10 @@ "database_name", "text", "target_lag", - "warehouse" + "warehouse", + "refresh_mode", + "initialize", + "comment" from table(result_scan(last_query_id())) {%- endset %} {% set _dynamic_table = run_query(_dynamic_table_sql) %} From 1c2f222d2b7acb83ca1f2c6807d764087ed19d4f Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Tue, 11 Jun 2024 21:37:48 -0400 Subject: [PATCH 05/13] incorporate new attributes into the changeset --- dbt/adapters/snowflake/relation.py | 14 ++++ .../snowflake/relation_configs/__init__.py | 2 + .../relation_configs/dynamic_table.py | 65 ++++++++++++++++--- 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/dbt/adapters/snowflake/relation.py b/dbt/adapters/snowflake/relation.py index ff94abc33..96ff5558a 100644 --- a/dbt/adapters/snowflake/relation.py +++ b/dbt/adapters/snowflake/relation.py @@ -9,6 +9,8 @@ from dbt.adapters.snowflake.relation_configs import ( SnowflakeDynamicTableConfig, SnowflakeDynamicTableConfigChangeset, + SnowflakeDynamicTableCommentConfigChange, + SnowflakeDynamicTableRefreshModeConfigChange, SnowflakeDynamicTableTargetLagConfigChange, SnowflakeDynamicTableWarehouseConfigChange, SnowflakeQuotePolicy, @@ -77,6 +79,18 @@ def dynamic_table_config_changeset( ) ) + if new_dynamic_table.refresh_mode != existing_dynamic_table.refresh_mode: + config_change_collection.refresh_mode = SnowflakeDynamicTableRefreshModeConfigChange( + action=RelationConfigChangeAction.create, + context=new_dynamic_table.refresh_mode, + ) + + if new_dynamic_table.comment != existing_dynamic_table.comment: + config_change_collection.comment = SnowflakeDynamicTableCommentConfigChange( + action=RelationConfigChangeAction.alter, + context=new_dynamic_table.comment, + ) + if config_change_collection.has_changes: return config_change_collection return None diff --git a/dbt/adapters/snowflake/relation_configs/__init__.py b/dbt/adapters/snowflake/relation_configs/__init__.py index e5ceabe49..40c9d3fe1 100644 --- a/dbt/adapters/snowflake/relation_configs/__init__.py +++ b/dbt/adapters/snowflake/relation_configs/__init__.py @@ -1,6 +1,8 @@ from dbt.adapters.snowflake.relation_configs.dynamic_table import ( SnowflakeDynamicTableConfig, SnowflakeDynamicTableConfigChangeset, + SnowflakeDynamicTableCommentConfigChange, + SnowflakeDynamicTableRefreshModeConfigChange, SnowflakeDynamicTableWarehouseConfigChange, SnowflakeDynamicTableTargetLagConfigChange, ) diff --git a/dbt/adapters/snowflake/relation_configs/dynamic_table.py b/dbt/adapters/snowflake/relation_configs/dynamic_table.py index f68fb6427..db030ca51 100644 --- a/dbt/adapters/snowflake/relation_configs/dynamic_table.py +++ b/dbt/adapters/snowflake/relation_configs/dynamic_table.py @@ -5,10 +5,31 @@ from dbt.adapters.relation_configs import RelationConfigChange, RelationResults from dbt.adapters.contracts.relation import RelationConfig from dbt.adapters.contracts.relation import ComponentName +from dbt_common.dataclass_schema import StrEnum # doesn't exist in standard library until py3.11 +from typing_extensions import Self from dbt.adapters.snowflake.relation_configs.base import SnowflakeRelationConfigBase +class RefreshMode(StrEnum): + AUTO = "AUTO" + FULL = "FULL" + INCREMENTAL = "INCREMENTAL" + + @classmethod + def default(cls) -> Self: + return cls("AUTO") + + +class Initialize(StrEnum): + ON_CREATE = "ON_CREATE" + ON_SCHEDULE = "ON_SCHEDULE" + + @classmethod + def default(cls) -> Self: + return cls("ON_CREATE") + + @dataclass(frozen=True, eq=True, unsafe_hash=True) class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): """ @@ -20,9 +41,9 @@ class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): - query: the query behind the table - target_lag: the maximum amount of time that the dynamic table’s content should lag behind updates to the base tables - snowflake_warehouse: the name of the warehouse that provides the compute resources for refreshing the dynamic table - - refresh_mode: specifies the refresh type for the dynamic table. + - refresh_mode: specifies the refresh type for the dynamic table - initialize: specifies the behavior of the initial refresh of the dynamic table - - comment: specifies a comment for the dynamic table. + - comment: specifies a comment for the dynamic table There are currently no non-configurable parameters. """ @@ -33,8 +54,8 @@ class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): query: str target_lag: str snowflake_warehouse: str - refresh_mode: Optional[str] = None - initialize: Optional[str] = None + refresh_mode: Optional[RefreshMode] = RefreshMode.default() + initialize: Optional[Initialize] = Initialize.default() comment: Optional[str] = None @classmethod @@ -65,11 +86,15 @@ def parse_relation_config(cls, relation_config: RelationConfig) -> Dict[str, Any "query": relation_config.compiled_code, "target_lag": relation_config.config.extra.get("target_lag"), "snowflake_warehouse": relation_config.config.extra.get("snowflake_warehouse"), - "refresh_mode": relation_config.config.extra.get("refresh_mode"), # type: ignore - "initialize": relation_config.config.extra.get("initialize"), # type: ignore - "comment": relation_config.config.extra.get("comment"), # type: ignore + "comment": relation_config.config.extra.get("comment"), } + if refresh_mode := relation_config.config.extra.get("refresh_mode"): + config_dict.update(refresh_mode=refresh_mode.upper()) + + if initialize := relation_config.config.extra.get("initialize"): + config_dict.update(initialize=initialize.upper()) + return config_dict @classmethod @@ -84,8 +109,8 @@ def parse_relation_results(cls, relation_results: RelationResults) -> Dict: "target_lag": dynamic_table.get("target_lag"), "snowflake_warehouse": dynamic_table.get("warehouse"), "refresh_mode": dynamic_table.get("refresh_mode"), - "initialize": dynamic_table.get("initialize"), "comment": dynamic_table.get("comment"), + # we don't get initialize since that's a one-time scheduler attribute, not a DT attribute } return config_dict @@ -109,10 +134,30 @@ def requires_full_refresh(self) -> bool: return False +@dataclass(frozen=True, eq=True, unsafe_hash=True) +class SnowflakeDynamicTableRefreshModeConfigChange(RelationConfigChange): + context: Optional[str] = None + + @property + def requires_full_refresh(self) -> bool: + return True + + +@dataclass(frozen=True, eq=True, unsafe_hash=True) +class SnowflakeDynamicTableCommentConfigChange(RelationConfigChange): + context: Optional[str] = None + + @property + def requires_full_refresh(self) -> bool: + return False + + @dataclass class SnowflakeDynamicTableConfigChangeset: target_lag: Optional[SnowflakeDynamicTableTargetLagConfigChange] = None snowflake_warehouse: Optional[SnowflakeDynamicTableWarehouseConfigChange] = None + refresh_mode: Optional[SnowflakeDynamicTableRefreshModeConfigChange] = None + comment: Optional[SnowflakeDynamicTableCommentConfigChange] = None @property def requires_full_refresh(self) -> bool: @@ -124,9 +169,11 @@ def requires_full_refresh(self) -> bool: if self.snowflake_warehouse else False ), + self.refresh_mode.requires_full_refresh if self.refresh_mode else False, + self.comment.requires_full_refresh if self.comment else False, ] ) @property def has_changes(self) -> bool: - return any([self.target_lag, self.snowflake_warehouse]) + return any([self.target_lag, self.snowflake_warehouse, self.refresh_mode, self.comment]) From c2905ccff06a034a106421f77bbf05e60eeb51fa Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Thu, 13 Jun 2024 18:51:25 -0400 Subject: [PATCH 06/13] add the new config to the dynamic table fixture --- tests/functional/adapter/dynamic_table_tests/files.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/adapter/dynamic_table_tests/files.py b/tests/functional/adapter/dynamic_table_tests/files.py index 6b449d476..a5f2d4ac4 100644 --- a/tests/functional/adapter/dynamic_table_tests/files.py +++ b/tests/functional/adapter/dynamic_table_tests/files.py @@ -27,6 +27,7 @@ materialized='dynamic_table', snowflake_warehouse='DBT_TESTING', target_lag='120 seconds', + refresh_mode='INCREMENTAL', ) }} select * from {{ ref('my_seed') }} """ From f44ebf664fa7ce486ad2ba0eef14b0f5a3a4b10e Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Thu, 13 Jun 2024 18:52:11 -0400 Subject: [PATCH 07/13] fix the test runner to inspect the object in the database for assertions --- .../test_dynamic_tables_changes.py | 101 ++++++------------ .../adapter/dynamic_table_tests/utils.py | 19 ++-- 2 files changed, 44 insertions(+), 76 deletions(-) diff --git a/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py b/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py index 984454fb9..8eac12ca9 100644 --- a/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py +++ b/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py @@ -17,6 +17,7 @@ MY_SEED, ) from tests.functional.adapter.dynamic_table_tests.utils import ( + query_refresh_mode, query_relation_type, query_target_lag, query_warehouse, @@ -25,15 +26,10 @@ class SnowflakeDynamicTableChanges: @staticmethod - def check_start_state(adapter, dynamic_table): - """ - This needs to be done manually for now until we fix the test suite's runner. The test suite's - runner cannot run queries with multiple statements. Snowflake's metadata is all behind `show` - and `describe` calls that require a second call to fetch the results; hence, the results - cannot be fetched. - """ - assert query_target_lag(adapter, dynamic_table) is None == "120 seconds" - assert query_warehouse(adapter, dynamic_table) is None == "DBT_TESTING" + def check_start_state(project, dynamic_table): + assert query_target_lag(project, dynamic_table) == "120 seconds" + assert query_warehouse(project, dynamic_table) == "DBT_TESTING" + assert query_refresh_mode(project, dynamic_table) == "INCREMENTAL" @staticmethod def change_config_via_alter(project, dynamic_table): @@ -52,26 +48,26 @@ def change_config_via_alter_downstream(project, dynamic_table): set_model_file(project, dynamic_table, new_model) @staticmethod - def check_state_alter_change_is_applied(adapter, dynamic_table): + def check_state_alter_change_is_applied(project, dynamic_table): # see above - assert query_target_lag(adapter, dynamic_table) == "5 minutes" - assert query_warehouse(adapter, dynamic_table) == "DBT_TESTING" + assert query_target_lag(project, dynamic_table) == "5 minutes" + assert query_warehouse(project, dynamic_table) == "DBT_TESTING" @staticmethod - def check_state_alter_change_is_applied_downstream(adapter, dynamic_table): + def check_state_alter_change_is_applied_downstream(project, dynamic_table): # see above - assert query_target_lag(adapter, dynamic_table) == "downstream" - assert query_warehouse(adapter, dynamic_table) == "DBT_TESTING" + assert query_target_lag(project, dynamic_table) == "downstream" + assert query_warehouse(project, dynamic_table) == "DBT_TESTING" @staticmethod def change_config_via_replace(project, dynamic_table): - # dbt-snowflake does not currently monitor any changes that trigger a full refresh - pass + initial_model = get_model_file(project, dynamic_table) + new_model = initial_model.replace("refresh_mode='INCREMENTAL'", "refresh_mode='FULL'") + set_model_file(project, dynamic_table, new_model) @staticmethod def check_state_replace_change_is_applied(project, dynamic_table): - # dbt-snowflake does not currently monitor any changes that trigger a full refresh - pass + assert query_refresh_mode(project, dynamic_table) == "FULL" @staticmethod def query_relation_type(project, relation: SnowflakeRelation) -> Optional[str]: @@ -103,6 +99,7 @@ def setup(self, project, my_dynamic_table): # the tests touch these files, store their contents in memory initial_model = get_model_file(project, my_dynamic_table) + self.check_start_state(project, my_dynamic_table) yield # and then reset them after the test runs @@ -117,7 +114,9 @@ def test_full_refresh_occurs_with_changes(self, project, my_dynamic_table): _, logs = run_dbt_and_capture( ["--debug", "run", "--models", my_dynamic_table.identifier, "--full-refresh"] ) - assert self.query_relation_type(project, my_dynamic_table) == "dynamic_table" + + self.check_state_alter_change_is_applied(project, my_dynamic_table) + self.check_state_replace_change_is_applied(project, my_dynamic_table) assert_message_in_logs( f"Applying ALTER to: {my_dynamic_table.render().upper()}", logs.replace('"', ""), False ) @@ -131,17 +130,11 @@ class TestSnowflakeDynamicTableChangesApply(SnowflakeDynamicTableChanges): def project_config_update(self): return {"models": {"on_configuration_change": OnConfigurationChangeOption.Apply.value}} - def test_change_is_applied_via_alter(self, project, adapter, my_dynamic_table): - """ - See above about the two commented assertions. In the meantime, these have been validated manually. - """ - # self.check_start_state(adapter, my_dynamic_table) - + def test_change_is_applied_via_alter(self, project, my_dynamic_table): self.change_config_via_alter(project, my_dynamic_table) _, logs = run_dbt_and_capture(["--debug", "run", "--models", my_dynamic_table.name]) - # self.check_state_alter_change_is_applied(adapter, my_dynamic_table) - + self.check_state_alter_change_is_applied(project, my_dynamic_table) assert_message_in_logs( f"Applying ALTER to: {my_dynamic_table.render().upper()}", logs.replace('"', "") ) @@ -151,17 +144,11 @@ def test_change_is_applied_via_alter(self, project, adapter, my_dynamic_table): False, ) - def test_change_is_applied_via_alter_downstream(self, project, adapter, my_dynamic_table): - """ - See above about the two commented assertions. In the meantime, these have been validated manually. - """ - # self.check_start_state(adapter, my_dynamic_table) - + def test_change_is_applied_via_alter_downstream(self, project, my_dynamic_table): self.change_config_via_alter_downstream(project, my_dynamic_table) _, logs = run_dbt_and_capture(["--debug", "run", "--models", my_dynamic_table.name]) - # self.check_state_alter_change_is_applied_downstream(adapter, my_dynamic_table) - + self.check_state_alter_change_is_applied_downstream(project, my_dynamic_table) assert_message_in_logs( f"Applying ALTER to: {my_dynamic_table.render().upper()}", logs.replace('"', "") ) @@ -171,19 +158,13 @@ def test_change_is_applied_via_alter_downstream(self, project, adapter, my_dynam False, ) - @pytest.mark.skip( - "dbt-snowflake does not currently monitor any changes the trigger a full refresh" - ) - def test_change_is_applied_via_replace(self, project, adapter, my_dynamic_table): - # self.check_start_state(adapter, my_dynamic_table) - + def test_change_is_applied_via_replace(self, project, my_dynamic_table): self.change_config_via_alter(project, my_dynamic_table) self.change_config_via_replace(project, my_dynamic_table) _, logs = run_dbt_and_capture(["--debug", "run", "--models", my_dynamic_table.name]) - # self.check_state_alter_change_is_applied(adapter, my_dynamic_table) - # self.check_state_replace_change_is_applied(adapter, my_dynamic_table) - + self.check_state_alter_change_is_applied(project, my_dynamic_table) + self.check_state_replace_change_is_applied(project, my_dynamic_table) assert_message_in_logs( f"Applying REPLACE to: {my_dynamic_table.render().upper()}", logs.replace('"', "") ) @@ -194,17 +175,11 @@ class TestSnowflakeDynamicTableChangesContinue(SnowflakeDynamicTableChanges): def project_config_update(self): return {"models": {"on_configuration_change": OnConfigurationChangeOption.Continue.value}} - def test_change_is_not_applied_via_alter(self, project, adapter, my_dynamic_table): - """ - See above about the two commented assertions. In the meantime, these have been validated manually. - """ - # self.check_start_state(adapter, my_dynamic_table) - + def test_change_is_not_applied_via_alter(self, project, my_dynamic_table): self.change_config_via_alter(project, my_dynamic_table) _, logs = run_dbt_and_capture(["--debug", "run", "--models", my_dynamic_table.name]) - # self.check_start_state(adapter, my_dynamic_table) - + self.check_start_state(project, my_dynamic_table) assert_message_in_logs( f"Configuration changes were identified and `on_configuration_change` was set" f" to `continue` for `{my_dynamic_table}`", @@ -219,15 +194,12 @@ def test_change_is_not_applied_via_alter(self, project, adapter, my_dynamic_tabl False, ) - def test_change_is_not_applied_via_replace(self, project, adapter, my_dynamic_table): - # self.check_start_state(adapter, my_dynamic_table) - + def test_change_is_not_applied_via_replace(self, project, my_dynamic_table): self.change_config_via_alter(project, my_dynamic_table) self.change_config_via_replace(project, my_dynamic_table) _, logs = run_dbt_and_capture(["--debug", "run", "--models", my_dynamic_table.name]) - # self.check_start_state(adapter, my_dynamic_table) - + self.check_start_state(project, my_dynamic_table) assert_message_in_logs( f"Configuration changes were identified and `on_configuration_change` was set" f" to `continue` for `{my_dynamic_table}`", @@ -249,18 +221,12 @@ def project_config_update(self): return {"models": {"on_configuration_change": OnConfigurationChangeOption.Fail.value}} def test_change_is_not_applied_via_alter(self, project, adapter, my_dynamic_table): - """ - See above about the two commented assertions. In the meantime, these have been validated manually. - """ - # self.check_start_state(adapter, my_dynamic_table) - self.change_config_via_alter(project, my_dynamic_table) _, logs = run_dbt_and_capture( ["--debug", "run", "--models", my_dynamic_table.name], expect_pass=False ) - # self.check_start_state(adapter, my_dynamic_table) - + self.check_start_state(adapter, my_dynamic_table) assert_message_in_logs( f"Configuration changes were identified and `on_configuration_change` was set" f" to `fail` for `{my_dynamic_table}`", @@ -276,16 +242,13 @@ def test_change_is_not_applied_via_alter(self, project, adapter, my_dynamic_tabl ) def test_change_is_not_applied_via_replace(self, project, adapter, my_dynamic_table): - # self.check_start_state(adapter, my_dynamic_table) - self.change_config_via_alter(project, my_dynamic_table) self.change_config_via_replace(project, my_dynamic_table) _, logs = run_dbt_and_capture( ["--debug", "run", "--models", my_dynamic_table.name], expect_pass=False ) - # self.check_start_state(adapter, my_dynamic_table) - + self.check_start_state(adapter, my_dynamic_table) assert_message_in_logs( f"Configuration changes were identified and `on_configuration_change` was set" f" to `fail` for `{my_dynamic_table}`", diff --git a/tests/functional/adapter/dynamic_table_tests/utils.py b/tests/functional/adapter/dynamic_table_tests/utils.py index 5763e1b6c..cd93ee1a3 100644 --- a/tests/functional/adapter/dynamic_table_tests/utils.py +++ b/tests/functional/adapter/dynamic_table_tests/utils.py @@ -30,19 +30,24 @@ def query_relation_type(project, relation: SnowflakeRelation) -> Optional[str]: return results[0].lower() -def query_target_lag(adapter, dynamic_table: SnowflakeRelation) -> Optional[str]: - config = describe_dynamic_table(adapter, dynamic_table) +def query_target_lag(project, dynamic_table: SnowflakeRelation) -> Optional[str]: + config = describe_dynamic_table(project, dynamic_table) return config.get("target_lag") -def query_warehouse(adapter, dynamic_table: SnowflakeRelation) -> Optional[str]: - config = describe_dynamic_table(adapter, dynamic_table) +def query_warehouse(project, dynamic_table: SnowflakeRelation) -> Optional[str]: + config = describe_dynamic_table(project, dynamic_table) return config.get("warehouse") -def describe_dynamic_table(adapter: BaseAdapter, dynamic_table: SnowflakeRelation) -> agate.Row: - with get_connection(adapter): - macro_results = adapter.execute_macro( +def query_refresh_mode(project, dynamic_table: SnowflakeRelation) -> Optional[str]: + config = describe_dynamic_table(project, dynamic_table) + return config.get("refresh_mode") + + +def describe_dynamic_table(project, dynamic_table: SnowflakeRelation) -> agate.Row: + with get_connection(project.adapter): + macro_results = project.adapter.execute_macro( "snowflake__describe_dynamic_table", kwargs={"relation": dynamic_table} ) config = macro_results["dynamic_table"] From 761a991addb9592064f6086e6ef9be5f5e20e05c Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Thu, 13 Jun 2024 19:36:13 -0400 Subject: [PATCH 08/13] add new attributes to the create and replace statements --- .../macros/relations/dynamic_table/create.sql | 18 +++++++++------- .../relations/dynamic_table/replace.sql | 21 +++++++++++++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/create.sql b/dbt/include/snowflake/macros/relations/dynamic_table/create.sql index bee08300a..981568cf8 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/create.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/create.sql @@ -1,16 +1,18 @@ {% macro snowflake__get_create_dynamic_table_as_sql(relation, sql) -%} + {%- set dynamic_table = relation.from_config(config.model) -%} + create dynamic table {{ relation }} - target_lag = '{{ config.get("target_lag") }}' - warehouse = {{ config.get("snowflake_warehouse") }} - {% if config.get("refresh_mode") %} - refresh_mode = '{{ config.get("refresh_mode") }}' + target_lag = '{{ dynamic_table.target_lag }}' + warehouse = {{ dynamic_table.snowflake_warehouse }} + {% if dynamic_table.refresh_mode %} + refresh_mode = {{ dynamic_table.refresh_mode }} {% endif %} - {% if config.get("initialize") %} - initialize = '{{ config.get("initialize") }}' + {% if dynamic_table.initialize %} + initialize = {{ dynamic_table.initialize }} {% endif %} - {% if config.get("comment") %} - comment = '{{ config.get("comment") }}' + {% if dynamic_table.comment %} + comment = '{{ dynamic_table.comment }}' {% endif %} as ( {{ sql }} diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql b/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql index 385ce119c..e49fb7cae 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql @@ -1,12 +1,21 @@ -{% macro snowflake__get_replace_dynamic_table_sql(relation, sql) %} +{% macro snowflake__get_replace_dynamic_table_sql(relation, sql) -%} + + {%- set dynamic_table = relation.from_config(config.model) -%} create or replace dynamic table {{ relation }} - target_lag = '{{ config.get("target_lag") }}' - warehouse = {{ config.get("snowflake_warehouse") }} + target_lag = '{{ dynamic_table.target_lag }}' + warehouse = {{ dynamic_table.snowflake_warehouse }} + {% if dynamic_table.refresh_mode %} + refresh_mode = {{ dynamic_table.refresh_mode }} + {% endif %} + {% if dynamic_table.initialize %} + initialize = {{ dynamic_table.initialize }} + {% endif %} + {% if dynamic_table.comment %} + comment = '{{ dynamic_table.comment }}' + {% endif %} as ( {{ sql }} ) - ; - {{ snowflake__refresh_dynamic_table(relation) }} -{% endmacro %} +{%- endmacro %} From c2bf2b3d3589d37231215c2fd8e303a5b4ee3ace Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Thu, 13 Jun 2024 19:38:19 -0400 Subject: [PATCH 09/13] add a method to create the config on the relation --- dbt/adapters/snowflake/relation.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/dbt/adapters/snowflake/relation.py b/dbt/adapters/snowflake/relation.py index 96ff5558a..0bfd4a8ca 100644 --- a/dbt/adapters/snowflake/relation.py +++ b/dbt/adapters/snowflake/relation.py @@ -2,9 +2,14 @@ from typing import FrozenSet, Optional, Type from dbt.adapters.base.relation import BaseRelation -from dbt.adapters.relation_configs import RelationConfigChangeAction, RelationResults +from dbt.adapters.relation_configs import ( + RelationConfigBase, + RelationConfigChangeAction, + RelationResults, +) from dbt.adapters.contracts.relation import RelationConfig from dbt.adapters.utils import classproperty +from dbt_common.exceptions import DbtRuntimeError from dbt.adapters.snowflake.relation_configs import ( SnowflakeDynamicTableConfig, @@ -23,6 +28,9 @@ class SnowflakeRelation(BaseRelation): type: Optional[SnowflakeRelationType] = None quote_policy: SnowflakeQuotePolicy = field(default_factory=lambda: SnowflakeQuotePolicy()) require_alias: bool = False + relation_configs = { + SnowflakeRelationType.DynamicTable: SnowflakeDynamicTableConfig, + } renameable_relations: FrozenSet[SnowflakeRelationType] = field( default_factory=lambda: frozenset( { @@ -54,6 +62,17 @@ def DynamicTable(cls) -> str: def get_relation_type(cls) -> Type[SnowflakeRelationType]: return SnowflakeRelationType + @classmethod + def from_config(cls, config: RelationConfig) -> RelationConfigBase: + relation_type: str = config.config.materialized + + if relation_config := cls.relation_configs.get(relation_type): + return relation_config.from_relation_config(config) + + raise DbtRuntimeError( + f"from_config() is not supported for the provided relation type: {relation_type}" + ) + @classmethod def dynamic_table_config_changeset( cls, relation_results: RelationResults, relation_config: RelationConfig From 08ddd425c5821f7f6b782c1e533da5c6bd1e8154 Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Thu, 13 Jun 2024 19:38:47 -0400 Subject: [PATCH 10/13] remove initialize from describe query --- .../macros/relations/dynamic_table/describe.sql | 1 - .../functional/adapter/dynamic_table_tests/files.py | 2 +- .../test_dynamic_tables_changes.py | 12 ++++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql b/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql index cc1553b9c..c8ba0c9a7 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql @@ -12,7 +12,6 @@ "target_lag", "warehouse", "refresh_mode", - "initialize", "comment" from table(result_scan(last_query_id())) {%- endset %} diff --git a/tests/functional/adapter/dynamic_table_tests/files.py b/tests/functional/adapter/dynamic_table_tests/files.py index a5f2d4ac4..ef8d2bf1f 100644 --- a/tests/functional/adapter/dynamic_table_tests/files.py +++ b/tests/functional/adapter/dynamic_table_tests/files.py @@ -26,7 +26,7 @@ {{ config( materialized='dynamic_table', snowflake_warehouse='DBT_TESTING', - target_lag='120 seconds', + target_lag='2 minutes', refresh_mode='INCREMENTAL', ) }} select * from {{ ref('my_seed') }} diff --git a/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py b/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py index 8eac12ca9..a3d163366 100644 --- a/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py +++ b/tests/functional/adapter/dynamic_table_tests/test_dynamic_tables_changes.py @@ -27,7 +27,7 @@ class SnowflakeDynamicTableChanges: @staticmethod def check_start_state(project, dynamic_table): - assert query_target_lag(project, dynamic_table) == "120 seconds" + assert query_target_lag(project, dynamic_table) == "2 minutes" assert query_warehouse(project, dynamic_table) == "DBT_TESTING" assert query_refresh_mode(project, dynamic_table) == "INCREMENTAL" @@ -35,7 +35,7 @@ def check_start_state(project, dynamic_table): def change_config_via_alter(project, dynamic_table): initial_model = get_model_file(project, dynamic_table) new_model = initial_model.replace( - "target_lag='120 seconds'", "target_lag='5 minutes'" + "target_lag='2 minutes'", "target_lag='5 minutes'" ) set_model_file(project, dynamic_table, new_model) @@ -43,7 +43,7 @@ def change_config_via_alter(project, dynamic_table): def change_config_via_alter_downstream(project, dynamic_table): initial_model = get_model_file(project, dynamic_table) new_model = initial_model.replace( - "target_lag='120 seconds'", "target_lag='downstream'" + "target_lag='2 minutes'", "target_lag='DOWNSTREAM'" ) set_model_file(project, dynamic_table, new_model) @@ -56,7 +56,7 @@ def check_state_alter_change_is_applied(project, dynamic_table): @staticmethod def check_state_alter_change_is_applied_downstream(project, dynamic_table): # see above - assert query_target_lag(project, dynamic_table) == "downstream" + assert query_target_lag(project, dynamic_table) == "DOWNSTREAM" assert query_warehouse(project, dynamic_table) == "DBT_TESTING" @staticmethod @@ -226,7 +226,7 @@ def test_change_is_not_applied_via_alter(self, project, adapter, my_dynamic_tabl ["--debug", "run", "--models", my_dynamic_table.name], expect_pass=False ) - self.check_start_state(adapter, my_dynamic_table) + self.check_start_state(project, my_dynamic_table) assert_message_in_logs( f"Configuration changes were identified and `on_configuration_change` was set" f" to `fail` for `{my_dynamic_table}`", @@ -248,7 +248,7 @@ def test_change_is_not_applied_via_replace(self, project, adapter, my_dynamic_ta ["--debug", "run", "--models", my_dynamic_table.name], expect_pass=False ) - self.check_start_state(adapter, my_dynamic_table) + self.check_start_state(project, my_dynamic_table) assert_message_in_logs( f"Configuration changes were identified and `on_configuration_change` was set" f" to `fail` for `{my_dynamic_table}`", From 86067704df1dbfdd5e9f2a54cd4d7fafbef904c4 Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Thu, 13 Jun 2024 19:41:05 -0400 Subject: [PATCH 11/13] update changelog entry --- .changes/unreleased/Features-20240131-125318.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/unreleased/Features-20240131-125318.yaml b/.changes/unreleased/Features-20240131-125318.yaml index eee5c07d6..75de2812e 100644 --- a/.changes/unreleased/Features-20240131-125318.yaml +++ b/.changes/unreleased/Features-20240131-125318.yaml @@ -1,5 +1,5 @@ kind: Features -body: Dynamic Table additional parameters +body: Support refresh_mode, initialize, and comment parameters for dynamic tables time: 2024-01-31T12:53:18.111616Z custom: Author: HenkvanDyk From c7781a45d08628d92a8d674c8cb2bc5f4bc20a64 Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Fri, 14 Jun 2024 14:09:57 -0400 Subject: [PATCH 12/13] remove comment from relation parsing since it's handled via persist_docs --- .../unreleased/Features-20240131-125318.yaml | 2 +- dbt/adapters/snowflake/relation.py | 7 ------- .../snowflake/relation_configs/__init__.py | 1 - .../relation_configs/dynamic_table.py | 18 +----------------- .../macros/relations/dynamic_table/create.sql | 3 --- .../relations/dynamic_table/describe.sql | 3 +-- .../macros/relations/dynamic_table/replace.sql | 3 --- 7 files changed, 3 insertions(+), 34 deletions(-) diff --git a/.changes/unreleased/Features-20240131-125318.yaml b/.changes/unreleased/Features-20240131-125318.yaml index 75de2812e..a2e3b8f51 100644 --- a/.changes/unreleased/Features-20240131-125318.yaml +++ b/.changes/unreleased/Features-20240131-125318.yaml @@ -1,5 +1,5 @@ kind: Features -body: Support refresh_mode, initialize, and comment parameters for dynamic tables +body: Support refresh_mode and initialize parameters for dynamic tables time: 2024-01-31T12:53:18.111616Z custom: Author: HenkvanDyk diff --git a/dbt/adapters/snowflake/relation.py b/dbt/adapters/snowflake/relation.py index 0bfd4a8ca..f477265f0 100644 --- a/dbt/adapters/snowflake/relation.py +++ b/dbt/adapters/snowflake/relation.py @@ -14,7 +14,6 @@ from dbt.adapters.snowflake.relation_configs import ( SnowflakeDynamicTableConfig, SnowflakeDynamicTableConfigChangeset, - SnowflakeDynamicTableCommentConfigChange, SnowflakeDynamicTableRefreshModeConfigChange, SnowflakeDynamicTableTargetLagConfigChange, SnowflakeDynamicTableWarehouseConfigChange, @@ -104,12 +103,6 @@ def dynamic_table_config_changeset( context=new_dynamic_table.refresh_mode, ) - if new_dynamic_table.comment != existing_dynamic_table.comment: - config_change_collection.comment = SnowflakeDynamicTableCommentConfigChange( - action=RelationConfigChangeAction.alter, - context=new_dynamic_table.comment, - ) - if config_change_collection.has_changes: return config_change_collection return None diff --git a/dbt/adapters/snowflake/relation_configs/__init__.py b/dbt/adapters/snowflake/relation_configs/__init__.py index 40c9d3fe1..62f95faff 100644 --- a/dbt/adapters/snowflake/relation_configs/__init__.py +++ b/dbt/adapters/snowflake/relation_configs/__init__.py @@ -1,7 +1,6 @@ from dbt.adapters.snowflake.relation_configs.dynamic_table import ( SnowflakeDynamicTableConfig, SnowflakeDynamicTableConfigChangeset, - SnowflakeDynamicTableCommentConfigChange, SnowflakeDynamicTableRefreshModeConfigChange, SnowflakeDynamicTableWarehouseConfigChange, SnowflakeDynamicTableTargetLagConfigChange, diff --git a/dbt/adapters/snowflake/relation_configs/dynamic_table.py b/dbt/adapters/snowflake/relation_configs/dynamic_table.py index b40f897e4..2e227d3a4 100644 --- a/dbt/adapters/snowflake/relation_configs/dynamic_table.py +++ b/dbt/adapters/snowflake/relation_configs/dynamic_table.py @@ -45,7 +45,6 @@ class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): - snowflake_warehouse: the name of the warehouse that provides the compute resources for refreshing the dynamic table - refresh_mode: specifies the refresh type for the dynamic table - initialize: specifies the behavior of the initial refresh of the dynamic table - - comment: specifies a comment for the dynamic table There are currently no non-configurable parameters. """ @@ -58,7 +57,6 @@ class SnowflakeDynamicTableConfig(SnowflakeRelationConfigBase): snowflake_warehouse: str refresh_mode: Optional[RefreshMode] = RefreshMode.default() initialize: Optional[Initialize] = Initialize.default() - comment: Optional[str] = None @classmethod def from_dict(cls, config_dict) -> "SnowflakeDynamicTableConfig": @@ -73,7 +71,6 @@ def from_dict(cls, config_dict) -> "SnowflakeDynamicTableConfig": "snowflake_warehouse": config_dict.get("snowflake_warehouse"), "refresh_mode": config_dict.get("refresh_mode"), "initialize": config_dict.get("initialize"), - "comment": config_dict.get("comment"), } dynamic_table: "SnowflakeDynamicTableConfig" = super().from_dict(kwargs_dict) @@ -88,7 +85,6 @@ def parse_relation_config(cls, relation_config: RelationConfig) -> Dict[str, Any "query": relation_config.compiled_code, "target_lag": relation_config.config.extra.get("target_lag"), "snowflake_warehouse": relation_config.config.extra.get("snowflake_warehouse"), - "comment": relation_config.config.extra.get("comment"), } if refresh_mode := relation_config.config.extra.get("refresh_mode"): @@ -111,7 +107,6 @@ def parse_relation_results(cls, relation_results: RelationResults) -> Dict: "target_lag": dynamic_table.get("target_lag"), "snowflake_warehouse": dynamic_table.get("warehouse"), "refresh_mode": dynamic_table.get("refresh_mode"), - "comment": dynamic_table.get("comment"), # we don't get initialize since that's a one-time scheduler attribute, not a DT attribute } @@ -145,21 +140,11 @@ def requires_full_refresh(self) -> bool: return True -@dataclass(frozen=True, eq=True, unsafe_hash=True) -class SnowflakeDynamicTableCommentConfigChange(RelationConfigChange): - context: Optional[str] = None - - @property - def requires_full_refresh(self) -> bool: - return False - - @dataclass class SnowflakeDynamicTableConfigChangeset: target_lag: Optional[SnowflakeDynamicTableTargetLagConfigChange] = None snowflake_warehouse: Optional[SnowflakeDynamicTableWarehouseConfigChange] = None refresh_mode: Optional[SnowflakeDynamicTableRefreshModeConfigChange] = None - comment: Optional[SnowflakeDynamicTableCommentConfigChange] = None @property def requires_full_refresh(self) -> bool: @@ -172,10 +157,9 @@ def requires_full_refresh(self) -> bool: else False ), self.refresh_mode.requires_full_refresh if self.refresh_mode else False, - self.comment.requires_full_refresh if self.comment else False, ] ) @property def has_changes(self) -> bool: - return any([self.target_lag, self.snowflake_warehouse, self.refresh_mode, self.comment]) + return any([self.target_lag, self.snowflake_warehouse, self.refresh_mode]) diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/create.sql b/dbt/include/snowflake/macros/relations/dynamic_table/create.sql index 981568cf8..253788779 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/create.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/create.sql @@ -11,9 +11,6 @@ {% if dynamic_table.initialize %} initialize = {{ dynamic_table.initialize }} {% endif %} - {% if dynamic_table.comment %} - comment = '{{ dynamic_table.comment }}' - {% endif %} as ( {{ sql }} ) diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql b/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql index c8ba0c9a7..cc79328fe 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/describe.sql @@ -11,8 +11,7 @@ "text", "target_lag", "warehouse", - "refresh_mode", - "comment" + "refresh_mode" from table(result_scan(last_query_id())) {%- endset %} {% set _dynamic_table = run_query(_dynamic_table_sql) %} diff --git a/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql b/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql index e49fb7cae..dbe27d66e 100644 --- a/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql +++ b/dbt/include/snowflake/macros/relations/dynamic_table/replace.sql @@ -11,9 +11,6 @@ {% if dynamic_table.initialize %} initialize = {{ dynamic_table.initialize }} {% endif %} - {% if dynamic_table.comment %} - comment = '{{ dynamic_table.comment }}' - {% endif %} as ( {{ sql }} ) From 3db741b21927ac101c84e04b4cd0d85d452e5eb7 Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Mon, 17 Jun 2024 11:50:05 -0400 Subject: [PATCH 13/13] add self to changelog --- .changes/unreleased/Features-20240131-125318.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/unreleased/Features-20240131-125318.yaml b/.changes/unreleased/Features-20240131-125318.yaml index a2e3b8f51..63771d71e 100644 --- a/.changes/unreleased/Features-20240131-125318.yaml +++ b/.changes/unreleased/Features-20240131-125318.yaml @@ -2,5 +2,5 @@ kind: Features body: Support refresh_mode and initialize parameters for dynamic tables time: 2024-01-31T12:53:18.111616Z custom: - Author: HenkvanDyk + Author: HenkvanDyk,mikealfare Issue: "1076"