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
27 changes: 27 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,25 @@ 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}
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
13 changes: 13 additions & 0 deletions dbt/adapters/databricks/relation.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ def render(self) -> str:
"""Return the type formatted for SQL statements (replace underscores with spaces)"""
return self.value.replace("_", " ").upper()

def render_for_alter(self) -> str:
"""Return the type formatted for ALTER statements.

Metric views use ALTER VIEW (not ALTER METRIC VIEW) syntax.
"""
if self == DatabricksRelationType.MetricView:
return "VIEW"
return self.render()


class DatabricksTableType(StrEnum):
External = "external"
Expand Down Expand Up @@ -117,6 +126,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
101 changes: 101 additions & 0 deletions dbt/adapters/databricks/relation_configs/metric_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from typing import ClassVar, Optional

from dbt.adapters.contracts.relation import RelationConfig
from dbt.adapters.relation_configs.config_base import RelationResults
from dbt_common.exceptions import DbtRuntimeError

from dbt.adapters.databricks.relation_configs.base import (
DatabricksComponentConfig,
DatabricksComponentProcessor,
DatabricksRelationConfigBase,
)
from dbt.adapters.databricks.relation_configs.tags import TagsProcessor
from dbt.adapters.databricks.relation_configs.tblproperties import TblPropertiesProcessor


class MetricViewQueryConfig(DatabricksComponentConfig):
"""Component encapsulating the YAML definition of a metric view."""

query: str

def get_diff(self, other: "MetricViewQueryConfig") -> Optional["MetricViewQueryConfig"]:
# Normalize whitespace for comparison
self_normalized = " ".join(self.query.split())
other_normalized = " ".join(other.query.split())
if self_normalized != other_normalized:
return self
return None


class MetricViewQueryProcessor(DatabricksComponentProcessor[MetricViewQueryConfig]):
"""Processor for metric view YAML definitions.

Metric views store their YAML definitions in information_schema.views, but wrapped
in $$ delimiters. This processor extracts and compares the YAML content.
"""

name: ClassVar[str] = "query"

@classmethod
def from_relation_results(cls, result: RelationResults) -> MetricViewQueryConfig:
from dbt.adapters.databricks.logging import logger

# Get the view text from DESCRIBE EXTENDED output
describe_extended = result.get("describe_extended")
if not describe_extended:
raise DbtRuntimeError(
f"Cannot find metric view description. Result keys: {list(result.keys())}"
)

# Find the "View Text" row in DESCRIBE EXTENDED output
view_definition = None
for row in describe_extended:
if row[0] == "View Text":
view_definition = row[1]
break

logger.debug(
f"MetricViewQueryProcessor: view_definition = "
f"{view_definition[:200] if view_definition else 'None'}"
)

if not view_definition:
raise DbtRuntimeError("Metric view has no 'View Text' in DESCRIBE EXTENDED output")

view_definition = view_definition.strip()

# Extract YAML content from $$ delimiters if present
# Format: $$ yaml_content $$
if "$$" in view_definition:
parts = view_definition.split("$$")
if len(parts) >= 2:
# The YAML is between the first and second $$ markers
view_definition = parts[1].strip()

return MetricViewQueryConfig(query=view_definition)

@classmethod
def from_relation_config(cls, relation_config: RelationConfig) -> MetricViewQueryConfig:
query = relation_config.compiled_code

if query:
return MetricViewQueryConfig(query=query.strip())
else:
raise DbtRuntimeError(
f"Cannot compile metric view {relation_config.identifier} with no YAML definition"
)


class MetricViewConfig(DatabricksRelationConfigBase):
"""Config for metric views.

Metric views use YAML definitions stored in information_schema.views wrapped in $$ delimiters.
Changes to the YAML definition can be applied via ALTER VIEW AS.
Tags and tblproperties can also be altered incrementally.
"""

config_components = [
TagsProcessor,
TblPropertiesProcessor,
MetricViewQueryProcessor,
]
46 changes: 46 additions & 0 deletions dbt/include/databricks/macros/materializations/metric_view.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{% 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 %}
{#- Only use alter path if existing relation is actually a metric_view -#}
{% if existing_relation.is_metric_view and relation_should_be_altered(existing_relation) %}
{% set configuration_changes = get_metric_view_configuration_changes(existing_relation) %}
{% if configuration_changes and configuration_changes.changes %}
{% if configuration_changes.requires_full_refresh %}
{{ replace_with_metric_view(existing_relation, target_relation) }}
{% else %}
{{ alter_metric_view(target_relation, configuration_changes.changes) }}
{% endif %}
{% else %}
{# No changes detected - run a no-op statement for dbt tracking #}
{% call statement('main') %}
select 1
{% endcall %}
{% endif %}
{% else %}
{{ replace_with_metric_view(existing_relation, target_relation) }}
{% endif %}
{% 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{% endmacro %}

{% macro get_alter_query_sql(target_relation, query) -%}
ALTER {{ target_relation.type.render() }} {{ target_relation.render() }} AS (
ALTER {{ target_relation.type.render_for_alter() }} {{ target_relation.render() }} AS (
{{ query }}
)
{%- endmacro %}
{%- endmacro %}
9 changes: 8 additions & 1 deletion dbt/include/databricks/macros/relations/config.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,11 @@
{%- set model_config = adapter.get_config_from_model(config.model) -%}
{%- set configuration_changes = model_config.get_changeset(existing_config) -%}
{% do return(configuration_changes) %}
{%- endmacro -%}
{%- endmacro -%}

{%- macro get_metric_view_configuration_changes(existing_relation) -%}
{%- 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
56 changes: 56 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,56 @@
{% 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") %}

{# Handle YAML definition changes via ALTER VIEW AS #}
{% if query %}
{% call statement('main') %}
{{ get_alter_metric_view_as_sql(target_relation, query.query) }}
{% endcall %}
{% else %}
{# Ensure statement('main') is called for dbt to track the run #}
{% call statement('main') %}
select 1
{% endcall %}
{% endif %}

{% if tags %}
{{ apply_tags(target_relation, tags.set_tags) }}
{% endif %}
{% if tblproperties %}
{{ apply_tblproperties(target_relation, tblproperties.tblproperties) }}
{% endif %}
{% endmacro %}

{% macro get_alter_metric_view_as_sql(relation, yaml_content) -%}
{{ adapter.dispatch('get_alter_metric_view_as_sql', 'dbt')(relation, yaml_content) }}
{%- endmacro %}

{% macro databricks__get_alter_metric_view_as_sql(relation, yaml_content) %}
alter view {{ relation.render() }} as $$
{{ yaml_content }}
$$
{% endmacro %}

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

{% if tblproperties %}
{{ apply_tblproperties(target_relation, tblproperties) }}
{% endif %}

{% 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
Loading
Loading