Skip to content

Commit 1368673

Browse files
authored
Migrate to the dbt catalog framework (#1012)
1 parent d9817a0 commit 1368673

18 files changed

Lines changed: 429 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Features
44

55
- Support constraint updates on incremental runs (with Materialization V2) ([1013](https://github.com/databricks/dbt-databricks/pull/1013))
6+
- Add catalog integration support - set table formats, file formats, and locations in `catalogs.yml` ([1012](https://github.com/databricks/dbt-databricks/pull/1012))
67

78
### Fixes
89

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from dbt.adapters.databricks.catalogs._hive_metastore import HiveMetastoreCatalogIntegration
2+
from dbt.adapters.databricks.catalogs._relation import DatabricksCatalogRelation
3+
from dbt.adapters.databricks.catalogs._unity import UnityCatalogIntegration
4+
5+
__all__ = [
6+
"DatabricksCatalogRelation",
7+
"HiveMetastoreCatalogIntegration",
8+
"UnityCatalogIntegration",
9+
]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from typing import Optional
2+
3+
from dbt.adapters.catalogs import CatalogIntegration, CatalogIntegrationConfig
4+
from dbt.adapters.contracts.relation import RelationConfig
5+
from dbt.adapters.databricks import constants, parse_model
6+
from dbt.adapters.databricks.catalogs._relation import DatabricksCatalogRelation
7+
8+
9+
class HiveMetastoreCatalogIntegration(CatalogIntegration):
10+
catalog_type = constants.HIVE_METASTORE_CATALOG_TYPE
11+
allows_writes = True
12+
13+
def __init__(self, config: CatalogIntegrationConfig) -> None:
14+
super().__init__(config)
15+
self.file_format: str = config.adapter_properties.get("file_format")
16+
17+
@property
18+
def location_root(self) -> Optional[str]:
19+
"""
20+
Volumes in Databricks are non-tabular datasets, which is something
21+
different from what we mean by "external_volume" in dbt.
22+
However, the protocol expects "external_volume" to be set.
23+
"""
24+
return self.external_volume
25+
26+
@location_root.setter
27+
def location_root(self, value: Optional[str]) -> None:
28+
self.external_volume = value
29+
30+
def build_relation(self, model: RelationConfig) -> DatabricksCatalogRelation:
31+
"""
32+
Args:
33+
model: `config.model` (not `model`) from the jinja context
34+
"""
35+
return DatabricksCatalogRelation(
36+
catalog_type=self.catalog_type,
37+
catalog_name=self.catalog_name,
38+
table_format=parse_model.table_format(model) or self.table_format,
39+
file_format=parse_model.file_format(model) or self.file_format,
40+
external_volume=parse_model.location_root(model) or self.external_volume,
41+
location_path=parse_model.location_path(model),
42+
)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import posixpath
2+
from dataclasses import dataclass
3+
from typing import Optional
4+
5+
from dbt_common.exceptions import DbtConfigError
6+
7+
from dbt.adapters.databricks import constants
8+
9+
10+
@dataclass
11+
class DatabricksCatalogRelation:
12+
catalog_type: str = constants.DEFAULT_CATALOG.catalog_type
13+
catalog_name: Optional[str] = constants.DEFAULT_CATALOG.name
14+
table_format: Optional[str] = constants.DEFAULT_CATALOG.adapter_properties.get("table_format")
15+
file_format: Optional[str] = constants.DEFAULT_CATALOG.adapter_properties.get("file_format")
16+
external_volume: Optional[str] = constants.DEFAULT_CATALOG.external_volume
17+
location_path: Optional[str] = None
18+
19+
@property
20+
def location_root(self) -> Optional[str]:
21+
"""
22+
Volumes in Databricks are non-tabular datasets, which is something
23+
different from what we mean by "external_volume" in dbt.
24+
However, the protocol expects "external_volume" to be set.
25+
"""
26+
return self.external_volume
27+
28+
@location_root.setter
29+
def location_root(self, value: Optional[str]) -> None:
30+
self.external_volume = value
31+
32+
@property
33+
def location(self) -> Optional[str]:
34+
if self.location_root and self.location_path:
35+
return posixpath.join(self.location_root, self.location_path)
36+
return None
37+
38+
@property
39+
def iceberg_table_properties(self) -> dict[str, str]:
40+
if self.table_format == constants.ICEBERG_TABLE_FORMAT:
41+
if self.file_format != constants.DELTA_FILE_FORMAT:
42+
raise DbtConfigError(
43+
"When table_format is 'iceberg', cannot set file_format to other than delta."
44+
)
45+
return {
46+
"delta.enableIcebergCompatV2": "true",
47+
"delta.universalFormat.enabledFormats": constants.ICEBERG_TABLE_FORMAT,
48+
}
49+
return {}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from typing import Optional
2+
3+
from dbt.adapters.catalogs import CatalogIntegration, CatalogIntegrationConfig
4+
from dbt.adapters.contracts.relation import RelationConfig
5+
from dbt.adapters.databricks import constants, parse_model
6+
from dbt.adapters.databricks.catalogs._relation import DatabricksCatalogRelation
7+
8+
9+
class UnityCatalogIntegration(CatalogIntegration):
10+
catalog_type = constants.UNITY_CATALOG_TYPE
11+
allows_writes = True
12+
13+
def __init__(self, config: CatalogIntegrationConfig) -> None:
14+
super().__init__(config)
15+
if location_root := config.adapter_properties.get("location_root"):
16+
self.external_volume: Optional[str] = location_root
17+
self.file_format: str = config.adapter_properties.get("file_format")
18+
19+
@property
20+
def location_root(self) -> Optional[str]:
21+
"""
22+
Volumes in Databricks are non-tabular datasets, which is something
23+
different from what we mean by "external_volume" in dbt.
24+
However, the protocol expects "external_volume" to be set.
25+
"""
26+
return self.external_volume
27+
28+
@location_root.setter
29+
def location_root(self, value: Optional[str]) -> None:
30+
self.external_volume = value
31+
32+
def build_relation(self, model: RelationConfig) -> DatabricksCatalogRelation:
33+
"""
34+
Args:
35+
model: `config.model` (not `model`) from the jinja context
36+
"""
37+
return DatabricksCatalogRelation(
38+
catalog_type=self.catalog_type,
39+
catalog_name=self.catalog_name,
40+
table_format=parse_model.table_format(model) or self.table_format,
41+
file_format=parse_model.file_format(model) or self.file_format,
42+
external_volume=parse_model.location_root(model) or self.external_volume,
43+
location_path=parse_model.location_path(model),
44+
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from types import SimpleNamespace
2+
3+
DEFAULT_TABLE_FORMAT = "default"
4+
ICEBERG_TABLE_FORMAT = "iceberg"
5+
6+
7+
DELTA_FILE_FORMAT = "delta"
8+
PARQUET_FILE_FORMAT = "parquet"
9+
HUDI_FILE_FORMAT = "hudi"
10+
11+
12+
UNITY_CATALOG_TYPE = "unity"
13+
HIVE_METASTORE_CATALOG_TYPE = "hive_metastore"
14+
15+
16+
DEFAULT_UNITY_CATALOG = SimpleNamespace(
17+
name="unity",
18+
catalog_name="default_unity",
19+
catalog_type=UNITY_CATALOG_TYPE,
20+
# requires the model to specify the external_volume (location_root) property
21+
external_volume=None,
22+
table_format=DEFAULT_TABLE_FORMAT,
23+
adapter_properties={
24+
"file_format": DELTA_FILE_FORMAT,
25+
},
26+
)
27+
DEFAULT_HIVE_METASTORE_CATALOG = SimpleNamespace(
28+
name="hive_metastore",
29+
catalog_name="default_hive_metastore",
30+
catalog_type=HIVE_METASTORE_CATALOG_TYPE,
31+
# requires the model to specify the external_volume (location_root) property
32+
external_volume=None,
33+
table_format=DEFAULT_TABLE_FORMAT,
34+
adapter_properties={
35+
"file_format": DELTA_FILE_FORMAT,
36+
},
37+
)
38+
DEFAULT_CATALOG = DEFAULT_UNITY_CATALOG

dbt/adapters/databricks/impl.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,20 @@
2323
from dbt.adapters.base.meta import available
2424
from dbt.adapters.base.relation import BaseRelation
2525
from dbt.adapters.capability import Capability, CapabilityDict, CapabilitySupport, Support
26+
from dbt.adapters.catalogs import CatalogRelation
2627
from dbt.adapters.contracts.connection import AdapterResponse, Connection
2728
from dbt.adapters.contracts.relation import RelationConfig, RelationType
28-
from dbt.adapters.databricks import constraints
29+
from dbt.adapters.databricks import constants, constraints, parse_model
2930
from dbt.adapters.databricks.behaviors.columns import (
3031
GetColumnsBehavior,
3132
GetColumnsByDescribe,
3233
GetColumnsByInformationSchema,
3334
)
35+
from dbt.adapters.databricks.catalogs import (
36+
DatabricksCatalogRelation,
37+
HiveMetastoreCatalogIntegration,
38+
UnityCatalogIntegration,
39+
)
3440
from dbt.adapters.databricks.column import DatabricksColumn
3541
from dbt.adapters.databricks.connections import DatabricksConnectionManager
3642
from dbt.adapters.databricks.global_state import GlobalState
@@ -180,6 +186,10 @@ class DatabricksAdapter(SparkAdapter):
180186
}
181187
)
182188

189+
CATALOG_INTEGRATIONS = [
190+
HiveMetastoreCatalogIntegration,
191+
UnityCatalogIntegration,
192+
]
183193
CONSTRAINT_SUPPORT = constraints.CONSTRAINT_SUPPORT
184194

185195
get_column_behavior: GetColumnsBehavior
@@ -196,6 +206,9 @@ def __init__(self, config: Any, mp_context: SpawnContext) -> None:
196206
except CompilationError:
197207
pass
198208

209+
self.add_catalog_integration(constants.DEFAULT_UNITY_CATALOG)
210+
self.add_catalog_integration(constants.DEFAULT_HIVE_METASTORE_CATALOG)
211+
199212
@property
200213
def _behavior_flags(self) -> list[BehaviorFlag]:
201214
return [USE_INFO_SCHEMA_FOR_COLUMNS, USE_USER_FOLDER_FOR_PYTHON, USE_MATERIALIZATION_V2]
@@ -205,20 +218,18 @@ def update_tblproperties_for_iceberg(
205218
self, config: BaseConfig, tblproperties: Optional[dict[str, str]] = None
206219
) -> dict[str, str]:
207220
result = tblproperties or config.get("tblproperties", {})
208-
if config.get("table_format") == TableFormat.ICEBERG:
221+
222+
catalog_relation: DatabricksCatalogRelation = self.build_catalog_relation(config.model) # type:ignore
223+
if catalog_relation.table_format == constants.ICEBERG_TABLE_FORMAT:
209224
if self.compare_dbr_version(14, 3) < 0:
210225
raise DbtConfigError("Iceberg support requires Databricks Runtime 14.3 or later.")
211-
if config.get("file_format", "delta") != "delta":
212-
raise DbtConfigError(
213-
"When table_format is 'iceberg', cannot set file_format to other than delta."
214-
)
215226
if config.get("materialized") not in ("incremental", "table", "snapshot"):
216227
raise DbtConfigError(
217228
"When table_format is 'iceberg', materialized must be 'incremental'"
218229
", 'table', or 'snapshot'."
219230
)
220-
result["delta.enableIcebergCompatV2"] = "true"
221-
result["delta.universalFormat.enabledFormats"] = "iceberg"
231+
result.update(catalog_relation.iceberg_table_properties)
232+
222233
return result
223234

224235
@available.parse(lambda *a, **k: 0)
@@ -820,6 +831,26 @@ def is_cluster(self) -> bool:
820831
def clean_sql(self, sql: str) -> str:
821832
return SqlUtils.clean_sql(sql)
822833

834+
@available
835+
def build_catalog_relation(self, model: RelationConfig) -> Optional[CatalogRelation]:
836+
"""
837+
Builds a relation for a given configuration.
838+
839+
This method uses the provided configuration to determine the appropriate catalog
840+
integration and config parser for building the relation. It defaults to the built-in Delta
841+
catalog if none is provided in the configuration for backward compatibility.
842+
843+
Args:
844+
model (RelationConfig): `config.model` (not `model`) from the jinja context
845+
846+
Returns:
847+
Any: The relation object generated through the catalog integration and parser
848+
"""
849+
if catalog := parse_model.catalog_name(model):
850+
catalog_integration = self.get_catalog_integration(catalog)
851+
return catalog_integration.build_relation(model)
852+
return None
853+
823854

824855
@dataclass(frozen=True)
825856
class RelationAPIBase(ABC, Generic[DatabricksRelationConfig]):
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import posixpath
2+
from typing import Optional
3+
4+
from dbt.adapters.contracts.relation import RelationConfig
5+
from dbt.adapters.databricks import constants
6+
7+
8+
def catalog_name(model: RelationConfig) -> Optional[str]:
9+
"""
10+
The user has not set the catalog, which is common as it's the legacy behavior.
11+
We need to infer a catalog based on the model config since macros now expect it.
12+
Luckily, all catalog attribution in the legacy behavior is on the model.
13+
This means we can take a default catalog and rely on the model overrides to supply the rest.
14+
"""
15+
return _get(model, "catalog") or constants.DEFAULT_CATALOG.name
16+
17+
18+
def file_format(model: RelationConfig) -> Optional[str]:
19+
return _get(model, "file_format")
20+
21+
22+
def location_path(model: RelationConfig) -> Optional[str]:
23+
if not model.config:
24+
return None
25+
26+
if model.config.get("include_full_name_in_path"):
27+
return posixpath.join(model.database, model.schema, model.identifier)
28+
return model.identifier
29+
30+
31+
def location_root(model: RelationConfig) -> Optional[str]:
32+
return _get(model, "location_root")
33+
34+
35+
def table_format(model: RelationConfig) -> Optional[str]:
36+
return _get(model, "table_format")
37+
38+
39+
def _get(
40+
model: RelationConfig, setting: str, case_sensitive: Optional[bool] = False
41+
) -> Optional[str]:
42+
if not model.config:
43+
return None
44+
45+
if value := model.config.get(setting):
46+
return value if case_sensitive else value.casefold()
47+
48+
return None
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from dbt_common.dataclass_schema import StrEnum
22

3+
from dbt.adapters.databricks import constants
4+
35

46
class TableFormat(StrEnum):
57
"""
@@ -8,8 +10,8 @@ class TableFormat(StrEnum):
810
simplify things for users.
911
"""
1012

11-
DEFAULT = "default"
12-
ICEBERG = "iceberg"
13+
DEFAULT = constants.DEFAULT_TABLE_FORMAT
14+
ICEBERG = constants.ICEBERG_TABLE_FORMAT
1315

1416
def __str__(self) -> str:
1517
return self.value

dbt/include/databricks/macros/materializations/incremental/incremental.sql

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
{% materialization incremental, adapter='databricks', supported_languages=['sql', 'python'] -%}
22
{{ log("MATERIALIZING INCREMENTAL") }}
3-
3+
4+
{%- set catalog_relation = adapter.build_catalog_relation(config.model) -%}
5+
46
{% set existing_relation = load_relation_with_metadata(this) %}
57
{% set target_relation = this.incorporate(type='table') %}
6-
{% set file_format = get_file_format() %}
7-
{% set incremental_strategy = get_incremental_strategy(file_format) %}
8+
{% set incremental_strategy = get_incremental_strategy(catalog_relation.file_format) %}
89
{% set grant_config = config.get('grants') %}
910
{% set full_refresh = should_full_refresh() %}
1011
{% set partition_by = config.get('partition_by') %}
1112
{% set language = model['language'] %}
1213
{% set on_schema_change = incremental_validate_on_schema_change(config.get('on_schema_change'), default='ignore') %}
13-
{% set is_delta = (file_format == 'delta' and existing_relation.is_delta) %}
14+
{% set is_delta = (catalog_relation.file_format == 'delta' and existing_relation.is_delta) %}
1415
{% set compiled_code = adapter.clean_sql(model['compiled_code']) %}
1516

1617
{% if adapter.behavior.use_materialization_v2 %}

0 commit comments

Comments
 (0)