Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f54e498
feat: use DESCRIBE TABLE AS JSON to replace info_schema queries
tejassp-db Apr 17, 2026
a5ac268
Parse json output
tejassp-db Apr 17, 2026
9664cd5
Refactor code and add docs.
tejassp-db Apr 21, 2026
1fe7103
chore: fix ruff line-length and mypy lint errors
tejassp-db Apr 27, 2026
ce1e4c9
Parse row filter from describe json output
tejassp-db Apr 29, 2026
9eb181b
Format code
tejassp-db Apr 29, 2026
53770b4
Fix mypy errors
tejassp-db Apr 29, 2026
14a6fc0
Lazy import agate to improve startup time
tejassp-db May 4, 2026
fc2b972
Fix constraint parser.
tejassp-db May 4, 2026
da62645
Format code
tejassp-db May 5, 2026
12dabc7
Gate describe json with a behavior flag
tejassp-db May 8, 2026
b140aa7
Add functional tests for describe json behavior flag.
tejassp-db May 8, 2026
7704ea6
Merge branch '1.12.latest' into replace-information-schema-with-descr…
sd-db May 9, 2026
d33d94d
More informative error messages on parse failures
tejassp-db May 13, 2026
a47fe80
More useful error message when parsing json output
tejassp-db May 13, 2026
fb46528
Rename use_describe_as_json -> use_describe_as_json_for_relation_meta…
tejassp-db May 13, 2026
eefacb0
Update CHANGELOG.md
tejassp-db May 13, 2026
2a60835
Merge branch '1.12.latest' into replace-information-schema-with-descr…
tejassp-db May 13, 2026
7456ae2
Merge branch '1.12.latest' into replace-information-schema-with-descr…
tejassp-db May 13, 2026
f99208e
Format code
tejassp-db May 14, 2026
bfb55fa
Reword changelog
tejassp-db May 14, 2026
7382b85
Remove unused tests
tejassp-db May 14, 2026
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: 2 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@
- Add support for row filters ([#1294](https://github.com/databricks/dbt-databricks/pull/1294))
- Add support for Python UDFs ([#1336](https://github.com/databricks/dbt-databricks/pull/1336))
- Add support for key-only `databricks_tags` for table and column tagging. This can now be configured by setting tag values to empty strings `""` or `None`. ([#1339](https://github.com/databricks/dbt-databricks/pull/1339))
- Fetch relation metadata like constraints, column masks, row filters, etc with a single `DESCRIBE TABLE EXTENDED ... AS JSON` call, replacing multiple `information_schema` queries. Falls back to `information_schema` on older runtimes. Gated behind `use_describe_as_json_for_relation_metadata` behavior flag, off by default. ([#1432](https://github.com/databricks/dbt-databricks/pull/1432))
- Support `SCHEDULE EVERY` and `TRIGGER ON UPDATE` refresh modes for materialized views and streaming tables, with parser and diff coverage so relations whose actual refresh is not CRON no longer crash on subsequent runs ([#1293](https://github.com/databricks/dbt-databricks/issues/1293))

### Fixes

- Fix `metric_view` failing with `METRIC_VIEW_INVALID_VIEW_DEFINITION` when models use bare `{{ ref(...) }}` for the `source:` field ([#1361](https://github.com/databricks/dbt-databricks/issues/1361))
- Fix `RefreshConfig.__eq__` self/other typo where two configs with the same `cron` but different `time_zone_value` compared equal
- Fix streaming-table DROP-SCHEDULE path that was silently filtered out of the changeset

### Fixes

- Fix `metric_view` failing with `METRIC_VIEW_INVALID_VIEW_DEFINITION` when models use bare `{{ ref(...) }}` for the `source:` field ([#1361](https://github.com/databricks/dbt-databricks/issues/1361))

### Under the Hood

- **BREAKING:** `databricks_tags` defined at different hierarchy levels (e.g. project-level and model-level) now merge additively instead of the child config completely replacing the parent.
Expand Down
12 changes: 8 additions & 4 deletions dbt/adapters/databricks/dbr_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
class DBRCapability(Enum):
"""Named capabilities that depend on DBR version."""

TIMESTAMPDIFF = "timestampdiff"
ICEBERG = "iceberg"
COMMENT_ON_COLUMN = "comment_on_column"
JSON_COLUMN_METADATA = "json_column_metadata"
STREAMING_TABLE_JSON_METADATA = "streaming_table_json_metadata"
DESCRIBE_TABLE_EXTENDED_AS_JSON = "describe_table_extended_as_json"
ICEBERG = "iceberg"
INSERT_BY_NAME = "insert_by_name"
JSON_COLUMN_METADATA = "json_column_metadata"
REPLACE_ON = "replace_on"
STREAMING_TABLE_JSON_METADATA = "streaming_table_json_metadata"
TIMESTAMPDIFF = "timestampdiff"


@dataclass
Expand Down Expand Up @@ -61,6 +62,9 @@ class DBRCapabilities:
DBRCapability.REPLACE_ON: CapabilitySpec(
min_version=(17, 1),
),
DBRCapability.DESCRIBE_TABLE_EXTENDED_AS_JSON: CapabilitySpec(
min_version=(17, 3),
),
}

def __init__(
Expand Down
550 changes: 528 additions & 22 deletions dbt/adapters/databricks/impl.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions dbt/adapters/databricks/relation.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ def is_metric_view(self) -> bool:
def is_streaming_table(self) -> bool:
return self.type == DatabricksRelationType.StreamingTable

@property
def is_foreign_table(self) -> bool:
return self.type == DatabricksRelationType.Foreign

@property
def is_external_table(self) -> bool:
return self.databricks_table_type == DatabricksTableType.External
Expand Down
10 changes: 9 additions & 1 deletion dbt/include/databricks/macros/adapters/metadata.sql
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,17 @@ SELECT
NULL
) AS databricks_table_type
FROM `system`.`information_schema`.`tables`
WHERE table_catalog = '{{ relation.database|lower }}'
WHERE table_catalog = '{{ relation.database|lower }}'
AND table_schema = '{{ relation.schema|lower }}'
{%- if relation.identifier %}
AND table_name = '{{ relation.identifier|lower }}'
{% endif %}
{% endmacro %}

{% macro describe_table_extended_as_json(relation) %}
{{ return(run_query_as(describe_table_extended_as_json_sql(relation), 'describe_table_extended_as_json')) }}
{% endmacro %}

{% macro describe_table_extended_as_json_sql(relation) %}
DESCRIBE TABLE EXTENDED {{ relation.render() }} AS JSON
{% endmacro %}
13 changes: 13 additions & 0 deletions tests/functional/adapter/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import pytest
from dbt.tests import util

from dbt.adapters.databricks.dbr_capabilities import DBRCapability

fail_if_tag_fetch_called_macros = """
{% macro fetch_tags(relation) %}
Expand Down Expand Up @@ -33,3 +36,13 @@ class ManagedIcebergMixin:
@pytest.fixture(scope="class")
def project_config_update(self):
return {"flags": {"use_managed_iceberg": True}}


class RequiresDescribeAsJsonCapabilityMixin:
"""Skip the test class if the connected compute lacks DESCRIBE TABLE EXTENDED AS JSON."""

@pytest.fixture(scope="class", autouse=True)
def require_describe_as_json_capability(self, project):
with util.get_connection(project.adapter):
if not project.adapter.has_capability(DBRCapability.DESCRIBE_TABLE_EXTENDED_AS_JSON):
pytest.skip("DESCRIBE TABLE EXTENDED AS JSON not supported on this compute")
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import pytest
from dbt.tests import util

from tests.functional.adapter.fixtures import MaterializationV2Mixin
from tests.functional.adapter.fixtures import (
MaterializationV2Mixin,
RequiresDescribeAsJsonCapabilityMixin,
)
from tests.functional.adapter.incremental import fixtures


Expand Down Expand Up @@ -61,3 +64,17 @@ def test_changing_column_masks(self, project):
assert result[0][1] == "hello" # name (unmasked)
assert result[0][2] == "********@example.com" # email (partially masked)
assert result[0][3] == "*****" # password (masked)


@pytest.mark.skip_profile("databricks_cluster")
class TestIncrementalColumnMasksDescribeJsonOn(
RequiresDescribeAsJsonCapabilityMixin, TestIncrementalColumnMasks
):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"flags": {
"use_materialization_v2": True,
"use_describe_as_json_for_relation_metadata": True,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from dbt.contracts.results import RunStatus
from dbt.tests import util

from tests.functional.adapter.fixtures import RequiresDescribeAsJsonCapabilityMixin
from tests.functional.adapter.incremental import fixtures


Expand Down Expand Up @@ -30,6 +31,20 @@ def test_add_non_null_constraint(self, project):
assert "DELTA_NOT_NULL_CONSTRAINT_VIOLATED" in results.results[0].message


@pytest.mark.skip_profile("databricks_cluster")
class TestIncrementalSetNonNullConstraintDescribeJsonOn(
RequiresDescribeAsJsonCapabilityMixin, TestIncrementalSetNonNullConstraint
):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"flags": {
"use_materialization_v2": True,
"use_describe_as_json_for_relation_metadata": True,
}
}


@pytest.mark.skip_profile("databricks_cluster")
class TestIncrementalUnsetNonNullConstraint:
@pytest.fixture(scope="class")
Expand Down Expand Up @@ -175,6 +190,20 @@ def test_update_primary_key_constraint(self, project):
assert not any(constraint[0] == "pk_model" for constraint in primary_key_constraints)


@pytest.mark.skip_profile("databricks_cluster")
class TestIncrementalUpdatePrimaryKeyConstraintDescribeJsonOn(
RequiresDescribeAsJsonCapabilityMixin, TestIncrementalUpdatePrimaryKeyConstraint
):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"flags": {
"use_materialization_v2": True,
"use_describe_as_json_for_relation_metadata": True,
}
}


@pytest.mark.skip_profile("databricks_cluster")
class TestCascadingConstraintDrop:
@pytest.fixture(scope="class")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
MaterializedViewChangesContinueMixin,
MaterializedViewChangesFailMixin,
)
from dbt_common.contracts.config.materialization import OnConfigurationChangeOption

from dbt.adapters.databricks.relation_configs.materialized_view import (
MaterializedViewConfig,
)
from dbt.adapters.databricks.relation_configs.tblproperties import TblPropertiesConfig
from tests.functional.adapter.fixtures import RequiresDescribeAsJsonCapabilityMixin
from tests.functional.adapter.materialized_view_tests import fixtures


Expand Down Expand Up @@ -86,6 +88,29 @@ class TestMaterializedViewApplyChanges(
pass


@pytest.mark.dlt
@pytest.mark.skip_profile("databricks_cluster", "databricks_uc_cluster")
class TestMaterializedViewApplyChangesDescribeJsonOn(
RequiresDescribeAsJsonCapabilityMixin, TestMaterializedViewApplyChanges
):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"models": {"on_configuration_change": OnConfigurationChangeOption.Apply.value},
"flags": {"use_describe_as_json_for_relation_metadata": True},
}

@pytest.mark.skip(reason="Full-refresh bypasses get_configuration_changes(); JSON path unused")
def test_full_refresh_occurs_with_changes(self):
pass

@pytest.mark.skip(
reason="Alter test only mutates refresh schedule. view_text is parsed but not asserted on"
)
def test_change_is_applied_via_alter(self):
pass


@pytest.mark.dlt
@pytest.mark.skip_profile("databricks_cluster", "databricks_uc_cluster")
class TestMaterializedViewContinueOnChanges(
Expand Down
48 changes: 47 additions & 1 deletion tests/functional/adapter/row_filters/test_row_filter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import pytest
from dbt.tests.util import run_dbt, write_file

from tests.functional.adapter.fixtures import MaterializationV1Mixin, MaterializationV2Mixin
from tests.functional.adapter.fixtures import (
MaterializationV1Mixin,
MaterializationV2Mixin,
RequiresDescribeAsJsonCapabilityMixin,
)
from tests.functional.adapter.row_filters.fixtures import (
base_model_mv,
base_model_sql,
Expand Down Expand Up @@ -142,6 +146,20 @@ def test_incremental_row_filter_lifecycle(self, project):
assert len(filters) == 0


@pytest.mark.skip_profile("databricks_cluster")
class TestIncrementalRowFilterDescribeJsonOn(
RequiresDescribeAsJsonCapabilityMixin, TestIncrementalRowFilter
):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"flags": {
"use_materialization_v2": True,
"use_describe_as_json_for_relation_metadata": True,
}
}


@pytest.mark.skip_profile("databricks_cluster", "databricks_uc_cluster")
class TestMaterializedViewRowFilter(RowFilterMixin):
"""Test row filters on materialized view models."""
Expand Down Expand Up @@ -177,6 +195,20 @@ def test_mv_row_filter_lifecycle(self, project):
assert len(filters) == 0


@pytest.mark.skip_profile("databricks_cluster", "databricks_uc_cluster")
class TestMaterializedViewRowFilterDescribeJsonOn(
RequiresDescribeAsJsonCapabilityMixin, TestMaterializedViewRowFilter
):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"flags": {
"use_materialization_v2": True,
"use_describe_as_json_for_relation_metadata": True,
}
}


@pytest.mark.skip_profile("databricks_cluster", "databricks_uc_cluster")
class TestStreamingTableRowFilter(RowFilterMixin):
"""Test row filters on streaming table models."""
Expand Down Expand Up @@ -228,6 +260,20 @@ def test_streaming_table_row_filter_lifecycle(self, project):
assert len(filters) == 0


@pytest.mark.skip_profile("databricks_cluster", "databricks_uc_cluster")
class TestStreamingTableRowFilterDescribeJsonOn(
RequiresDescribeAsJsonCapabilityMixin, TestStreamingTableRowFilter
):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"flags": {
"use_materialization_v2": True,
"use_describe_as_json_for_relation_metadata": True,
}
}


@pytest.mark.skip_profile("databricks_cluster")
class TestViewRowFilterFailure(MaterializationV2Mixin):
"""Test that row filters on regular views fail with clear error."""
Expand Down
22 changes: 22 additions & 0 deletions tests/functional/adapter/views/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from agate import Row
from dbt.tests import util

from tests.functional.adapter.fixtures import RequiresDescribeAsJsonCapabilityMixin
from tests.functional.adapter.views import fixtures


Expand Down Expand Up @@ -127,6 +128,27 @@ def project_config_update(self):
}


@pytest.mark.skip_profile("databricks_cluster")
class TestUpdateViewViaAlterDescriptionDescribeJsonOn(
RequiresDescribeAsJsonCapabilityMixin, TestUpdateViewViaAlterDescription
):
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"flags": {
"use_materialization_v2": True,
"use_describe_as_json_for_relation_metadata": True,
},
"models": {
"+view_update_via_alter": True,
"+persist_docs": {
"relation": True,
"columns": True,
},
},
}


@pytest.mark.skip_profile("databricks_cluster")
class TestUpdateViewViaAlterNothing(BaseUpdateNothing):
@pytest.fixture(scope="class")
Expand Down
7 changes: 7 additions & 0 deletions tests/unit/macros/adapters/test_metadata_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ def test_check_schema_exists_sql_with_hyphenated_database(self, template_bundle)
expected_sql = "SHOW SCHEMAS IN `data_engineering-uc-dev` LIKE 'my_schema'"
self.assert_sql_equal(result, expected_sql)

def test_describe_table_extended_as_json_sql(self, template_bundle, relation):
result = self.run_macro(
template_bundle.template, "describe_table_extended_as_json_sql", relation
)
expected_sql = "DESCRIBE TABLE EXTENDED `some_database`.`some_schema`.`some_table` AS JSON"
self.assert_sql_equal(result, expected_sql)

def test_case_sensitivity(self, template_bundle):
relation = Mock()
relation.database = "TEST_DB"
Expand Down
Loading
Loading