Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
## dbt-databricks 1.12.0 (TBD)

- Add support for metric views as a materialization ([1285](https://github.com/databricks/dbt-databricks/pull/1285))

## dbt-databricks 1.11.4 (TBD)

### Features

- Add `query_id` to `SQLQueryStatus` events to improve query tracing and debugging

### Fixes
- Fix `hard_deletes: invalidate` incorrectly invalidating active records in snapshots (thanks @Zurbste!) ([#1281](https://github.com/databricks/dbt-databricks/issues/1281))

- Fix `hard_deletes: invalidate` incorrectly invalidating active records in snapshots (thanks @Zurbste!) ([#1281](https://github.com/databricks/dbt-databricks/issues/1281))

## dbt-databricks 1.11.3 (Dec 5, 2025)

Expand Down
31 changes: 31 additions & 0 deletions dbt/adapters/databricks/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
from dbt.adapters.databricks.relation_configs.materialized_view import (
MaterializedViewConfig,
)
from dbt.adapters.databricks.relation_configs.metric_view import MetricViewConfig
from dbt.adapters.databricks.relation_configs.streaming_table import (
StreamingTableConfig,
)
Expand Down Expand Up @@ -919,6 +920,8 @@ def get_relation_config(self, relation: DatabricksRelation) -> DatabricksRelatio
return IncrementalTableAPI.get_from_relation(self, relation)
elif relation.type == DatabricksRelationType.View:
return ViewAPI.get_from_relation(self, relation)
elif relation.type == DatabricksRelationType.MetricView:
return MetricViewAPI.get_from_relation(self, relation)
else:
raise NotImplementedError(f"Relation type {relation.type} is not supported.")

Expand All @@ -934,6 +937,8 @@ def get_config_from_model(self, model: RelationConfig) -> DatabricksRelationConf
return IncrementalTableAPI.get_from_relation_config(model)
elif model.config.materialized == "view":
return ViewAPI.get_from_relation_config(model)
elif model.config.materialized == "metric_view":
return MetricViewAPI.get_from_relation_config(model)
else:
raise NotImplementedError(
f"Materialization {model.config.materialized} is not supported."
Expand Down Expand Up @@ -1152,3 +1157,29 @@ def _describe_relation(
DESCRIBE_TABLE_EXTENDED_MACRO_NAME, kwargs=kwargs
)
return results


class MetricViewAPI(RelationAPIBase[MetricViewConfig]):
relation_type = DatabricksRelationType.MetricView

@classmethod
def config_type(cls) -> type[MetricViewConfig]:
return MetricViewConfig

@classmethod
def _describe_relation(
cls, adapter: DatabricksAdapter, relation: DatabricksRelation
) -> RelationResults:
results = {}
kwargs = {"relation": relation}
# Metric views are stored as views in information_schema but have different properties
results["information_schema.views"] = get_first_row(
adapter.execute_macro("get_view_description", kwargs=kwargs)
)
results["information_schema.tags"] = adapter.execute_macro("fetch_tags", kwargs=kwargs)
results["show_tblproperties"] = adapter.execute_macro("fetch_tbl_properties", kwargs=kwargs)
kwargs = {"table_name": relation}
results["describe_extended"] = adapter.execute_macro(
DESCRIBE_TABLE_EXTENDED_MACRO_NAME, kwargs=kwargs
)
return results
4 changes: 4 additions & 0 deletions dbt/adapters/databricks/relation.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ def is_hive_metastore(self) -> bool:
def is_materialized_view(self) -> bool:
return self.type == DatabricksRelationType.MaterializedView

@property
def is_metric_view(self) -> bool:
return self.type == DatabricksRelationType.MetricView

@property
def is_streaming_table(self) -> bool:
return self.type == DatabricksRelationType.StreamingTable
Expand Down
52 changes: 52 additions & 0 deletions dbt/adapters/databricks/relation_configs/metric_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import Optional

from typing_extensions import Self

from dbt.adapters.databricks.logging import logger
from dbt.adapters.databricks.relation_configs.base import (
DatabricksRelationChangeSet,
DatabricksRelationConfigBase,
)
from dbt.adapters.databricks.relation_configs.column_comments import ColumnCommentsProcessor
from dbt.adapters.databricks.relation_configs.column_tags import ColumnTagsProcessor
from dbt.adapters.databricks.relation_configs.comment import CommentProcessor
from dbt.adapters.databricks.relation_configs.query import QueryProcessor
from dbt.adapters.databricks.relation_configs.tags import TagsProcessor
from dbt.adapters.databricks.relation_configs.tblproperties import TblPropertiesProcessor


class MetricViewConfig(DatabricksRelationConfigBase):
config_components = [
TagsProcessor,
TblPropertiesProcessor,
QueryProcessor,
CommentProcessor,
ColumnCommentsProcessor,
ColumnTagsProcessor,
]

def get_changeset(self, existing: Self) -> Optional[DatabricksRelationChangeSet]:
changeset = super().get_changeset(existing)
if changeset:
# Metric views: query changes require full refresh (can't ALTER YAML definition)
if "query" in changeset.changes:
logger.debug(
"Metric view YAML definition changed, requiring replace, as there is"
" no API to update the YAML specification via ALTER."
)
changeset.requires_full_refresh = True
# Comment changes also require full refresh for metric views
if "comment" in changeset.changes:
logger.debug(
"Metric view description changed, requiring replace, as there is"
" no API yet to update comments."
)
changeset.requires_full_refresh = True
# Column comment changes require full refresh for metric views
if "column_comments" in changeset.changes:
logger.debug(
"Metric view column comments changed, requiring replace, as there is"
" no API to update column comments for metric views."
)
changeset.requires_full_refresh = True
return changeset
31 changes: 31 additions & 0 deletions dbt/include/databricks/macros/materializations/metric_view.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% materialization metric_view, adapter='databricks' -%}
{%- set existing_relation = load_relation_with_metadata(this) -%}
{%- set target_relation = this.incorporate(type='metric_view') -%}
{% set grant_config = config.get('grants') %}
{% set tags = config.get('databricks_tags') %}
{% set sql = adapter.clean_sql(sql) %}

{{ run_pre_hooks() }}

{% if existing_relation %}
{#- Metric views always use CREATE OR REPLACE - no alter path for now -#}
{{ log('Using replace_with_metric_view (metric views always use replace)') }}
{{ replace_with_metric_view(existing_relation, target_relation) }}
{% else %}
{% call statement('main') -%}
{{ get_create_metric_view_as_sql(target_relation, sql) }}
{%- endcall %}
{{ apply_tags(target_relation, tags) }}
{% set column_tags = adapter.get_column_tags_from_model(config.model) %}
{% if column_tags and column_tags.set_column_tags %}
{{ apply_column_tags(target_relation, column_tags) }}
{% endif %}
{% endif %}

{% set should_revoke = should_revoke(existing_relation, full_refresh_mode=True) %}
{% do apply_grants(target_relation, grant_config, should_revoke=True) %}

{{ run_post_hooks() }}

{{ return({'relations': [target_relation]}) }}
{%- endmaterialization %}
2 changes: 1 addition & 1 deletion dbt/include/databricks/macros/materializations/view.sql
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@

{% macro relation_should_be_altered(existing_relation) %}
{% set update_via_alter = config.get('view_update_via_alter', False) | as_bool %}
{% if existing_relation.is_view and update_via_alter %}
{% if (existing_relation.is_view or existing_relation.is_metric_view) and update_via_alter %}
{% if existing_relation.is_hive_metastore() %}
{{ exceptions.raise_compiler_error("Cannot update a view in the Hive metastore via ALTER VIEW. Please set `view_update_via_alter: false` in your model configuration.") }}
{% endif %}
Expand Down
9 changes: 9 additions & 0 deletions dbt/include/databricks/macros/relations/config.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,13 @@
{%- set model_config = adapter.get_config_from_model(config.model) -%}
{%- set configuration_changes = model_config.get_changeset(existing_config) -%}
{% do return(configuration_changes) %}
{%- endmacro -%}

{#- Metric view specific config changes - needed because metric views are stored as VIEWs in DB -#}
{%- macro get_metric_view_configuration_changes(existing_relation) -%}
{#- existing_relation should already be typed as metric_view via incorporate() -#}
{%- set existing_config = adapter.get_relation_config(existing_relation) -%}
{%- set model_config = adapter.get_config_from_model(config.model) -%}
{%- set configuration_changes = model_config.get_changeset(existing_config) -%}
{% do return(configuration_changes) %}
{%- endmacro -%}
3 changes: 3 additions & 0 deletions dbt/include/databricks/macros/relations/create.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
{%- elif relation.is_streaming_table -%}
{{ get_create_streaming_table_as_sql(relation, sql) }}

{%- elif relation.is_metric_view -%}
{{ get_create_metric_view_as_sql(relation, sql) }}

{%- else -%}
{{- exceptions.raise_compiler_error("`get_create_sql` has not been implemented for: " ~ relation.type ) -}}

Expand Down
2 changes: 1 addition & 1 deletion dbt/include/databricks/macros/relations/drop.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{{ drop_materialized_view(relation) }}
{%- elif relation.is_streaming_table-%}
{{ drop_streaming_table(relation) }}
{%- elif relation.is_view -%}
{%- elif relation.is_view or relation.is_metric_view -%}
{{ drop_view(relation) }}
{%- else -%}
{{ drop_table(relation) }}
Expand Down
36 changes: 36 additions & 0 deletions dbt/include/databricks/macros/relations/metric_view/alter.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{% macro alter_metric_view(target_relation, changes) %}
{{ log("Updating metric view via ALTER") }}
{{ adapter.dispatch('alter_metric_view', 'dbt')(target_relation, changes) }}
{% endmacro %}

{% macro databricks__alter_metric_view(target_relation, changes) %}
{% set tags = changes.get("tags") %}
{% set tblproperties = changes.get("tblproperties") %}
{% set query = changes.get("query") %}
{% set column_comments = changes.get("column_comments") %}

{# For metric views, only tags and tblproperties can be altered without recreating #}
{% if tags %}
{{ apply_tags(target_relation, tags.set_tags) }}
{% endif %}
{% if tblproperties %}
{{ apply_tblproperties(target_relation, tblproperties.tblproperties) }}
{% endif %}

{# Query changes and column comment changes require full refresh for metric views #}
{% if query or column_comments %}
{{ exceptions.warn("Metric view YAML definition or column comment changes detected that cannot be applied via ALTER. These changes will require CREATE OR REPLACE on next run.") }}
{% endif %}
{% endmacro %}

{% macro replace_with_metric_view(existing_relation, target_relation) %}
{% set sql = adapter.clean_sql(sql) %}
{% set tags = config.get('databricks_tags') %}
{{ execute_multiple_statements(get_replace_sql(existing_relation, target_relation, sql)) }}
{%- do apply_tags(target_relation, tags) -%}

{% set column_tags = adapter.get_column_tags_from_model(config.model) %}
{% if column_tags and column_tags.set_column_tags %}
{{ apply_column_tags(target_relation, column_tags) }}
{% endif %}
{% endmacro %}
12 changes: 12 additions & 0 deletions dbt/include/databricks/macros/relations/metric_view/create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% macro get_create_metric_view_as_sql(relation, sql) -%}
{{ adapter.dispatch('get_create_metric_view_as_sql', 'dbt')(relation, sql) }}
{%- endmacro %}

{% macro databricks__get_create_metric_view_as_sql(relation, sql) %}
create or replace view {{ relation.render() }}
with metrics
language yaml
as $$
{{ sql }}
$$
{% endmacro %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% macro get_replace_metric_view_sql(target_relation, sql) %}
{{ adapter.dispatch('get_replace_metric_view_sql', 'dbt')(target_relation, sql) }}
{% endmacro %}

{% macro databricks__get_replace_metric_view_sql(target_relation, sql) %}
{{ get_create_metric_view_as_sql(target_relation, sql) }}
{% endmacro %}
6 changes: 6 additions & 0 deletions dbt/include/databricks/macros/relations/replace.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
{{ exceptions.raise_not_implemented('get_replace_sql not implemented for target of table') }}
{% endif %}

{#- Metric views always support CREATE OR REPLACE (no delta/file_format dependency) -#}
{#- Note: existing relation is typed as VIEW from DB, so check target for metric_view -#}
{% if target_relation.is_metric_view %}
{{ return(get_replace_metric_view_sql(target_relation, sql)) }}
{% endif %}

{% set safe_replace = config.get('use_safer_relation_operations', False) | as_bool %}
{% set file_format = adapter.resolve_file_format(config) %}
{% set is_replaceable = existing_relation.type == target_relation.type and existing_relation.can_be_replaced and file_format == "delta" %}
Expand Down
5 changes: 5 additions & 0 deletions dbt/include/databricks/macros/relations/tags.sql
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@
{%- endmacro -%}

{% macro alter_set_tags(relation, tags) -%}
{#- Metric views use ALTER VIEW syntax, not ALTER METRIC VIEW -#}
{%- if relation.is_metric_view -%}
ALTER VIEW {{ relation.render() }} SET TAGS (
{%- else -%}
ALTER {{ relation.type.render() }} {{ relation.render() }} SET TAGS (
{%- endif -%}
{% for tag in tags -%}
'{{ tag }}' = '{{ tags[tag] }}' {%- if not loop.last %}, {% endif -%}
{%- endfor %}
Expand Down
63 changes: 63 additions & 0 deletions tests/functional/adapter/metric_views/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
source_table = """
{{ config(materialized='table') }}

select 1 as id, 100 as revenue, 'completed' as status, '2024-01-01' as order_date
union all
select 2 as id, 200 as revenue, 'pending' as status, '2024-01-02' as order_date
union all
select 3 as id, 150 as revenue, 'completed' as status, '2024-01-03' as order_date
"""

basic_metric_view = """
{{ config(materialized='metric_view') }}

version: 0.1
source: "{{ ref('source_orders') }}"
dimensions:
- name: order_date
expr: order_date
- name: status
expr: status
measures:
- name: total_orders
expr: count(1)
- name: total_revenue
expr: sum(revenue)
"""

metric_view_with_filter = """
{{ config(materialized='metric_view') }}

version: 0.1
source: "{{ ref('source_orders') }}"
filter: status = 'completed'
dimensions:
- name: order_date
expr: order_date
measures:
- name: completed_orders
expr: count(1)
- name: completed_revenue
expr: sum(revenue)
"""

metric_view_with_config = """
{{
config(
materialized='metric_view',
databricks_tags={
'team': 'analytics',
'environment': 'test'
}
)
}}

version: 0.1
source: "{{ ref('source_orders') }}"
dimensions:
- name: status
expr: status
measures:
- name: order_count
expr: count(1)
"""
Loading
Loading