Skip to content

Commit 69017ab

Browse files
committed
Merge branch 'main' into 1.10.latest
2 parents 02b01cb + dab5794 commit 69017ab

36 files changed

Lines changed: 1769 additions & 77 deletions

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# the repo. Unless a later match takes precedence, these
33
# users will be requested for review when someone opens a
44
# pull request.
5-
* @andrefurlan-db @susodapop @benc-db @rcypher-databricks
5+
* @benc-db @ericj-db

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
## dbt-databricks 1.10.3 (TBD)
2+
3+
## dbt-databricks 1.10.2 (May 21, 2025)
4+
5+
### Features
6+
7+
- Support constraint updates on incremental runs (with Materialization V2) ([1013](https://github.com/databricks/dbt-databricks/pull/1013))
8+
- Add catalog integration support - set table formats, file formats, and locations in `catalogs.yml` ([1012](https://github.com/databricks/dbt-databricks/pull/1012))
9+
10+
### Fixes
11+
12+
- Fix bug with multiple not_null constraints defined on the model level ([1008](https://github.com/databricks/dbt-databricks/pull/1008))
13+
- Fix bug with temp tables not being dropped after python model is materialized ([1010](https://github.com/databricks/dbt-databricks/issues/1010))
14+
- Fix bug with alter view dispatch where dbt could not find the approprate macro ([1029](https://github.com/databricks/dbt-databricks/pull/1029))
15+
116
## dbt-databricks 1.10.1 (Apr 29, 2025)
217

318
### Fixes
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version = "1.10.1"
1+
version = "1.10.2"

dbt/adapters/databricks/api_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ def _get_exception(self, response: Response) -> None:
354354
result_state = state.get("result_state")
355355
life_cycle_state = state["life_cycle_state"]
356356

357-
if result_state is not None and result_state != "SUCCESS":
357+
if result_state == "CANCELED":
358358
raise DbtRuntimeError(f"Python model run ended in result_state {result_state}")
359359

360360
if life_cycle_state != "TERMINATED":
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/constraints.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from abc import ABC, abstractmethod
2+
from dataclasses import dataclass
23
from typing import Any, ClassVar, Optional, TypeVar
34
from uuid import uuid4
45

@@ -27,6 +28,7 @@
2728
T = TypeVar("T", bound="TypedConstraint")
2829

2930

31+
@dataclass
3032
class TypedConstraint(ModelLevelConstraint, ABC):
3133
"""Constraint that enforces type because it has render logic"""
3234

@@ -61,6 +63,19 @@ def _render_error(self, missing: list[list[str]]) -> DbtValidationError:
6163
f"{self.str_type} constraint '{name}' is missing required field(s): {fields}"
6264
)
6365

66+
# Enables set equality checks, especially for convenient unit testing
67+
def __hash__(self) -> int:
68+
# Create a tuple of all the fields that should be used for equality comparison
69+
fields = (
70+
self.type,
71+
self.name,
72+
tuple(self.columns) if self.columns else None,
73+
self.expression,
74+
self.to,
75+
tuple(self.to_columns) if self.to_columns else None,
76+
)
77+
return hash(fields)
78+
6479

6580
class CustomConstraint(TypedConstraint):
6681
str_type = "custom"
@@ -203,7 +218,7 @@ def parse_model_constraints(
203218
if constraint["type"] == ConstraintType.not_null:
204219
if not constraint.get("columns"):
205220
raise DbtValidationError("not_null constraint on model must have 'columns' defined")
206-
column_names.add(*constraint["columns"])
221+
column_names.update(constraint["columns"])
207222
else:
208223
constraints.append(parse_constraint(constraint))
209224

0 commit comments

Comments
 (0)