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
5 changes: 4 additions & 1 deletion dbt/adapters/databricks/catalogs/_relation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ def location_root(self, value: Optional[str]) -> None:
@property
def location(self) -> Optional[str]:
if self.location_root and self.location_path:
return posixpath.join(self.location_root, self.location_path)
result = posixpath.join(self.location_root, self.location_path)
# Return None if result is empty string to avoid "Can not create a Path from an empty string" errors
# See: https://github.com/databricks/dbt-databricks/issues/1228
return result if result else None
return None

@property
Expand Down
4 changes: 3 additions & 1 deletion dbt/include/databricks/macros/relations/location.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
--#}
{%- if relation.catalog_type is not none -%}

{%- if relation.location is not none -%}
{%- if relation.location is not none and relation.location != '' -%}
location '{{ relation.location }}{% if is_incremental() %}_tmp{% endif %}'
{%- endif -%}

Expand All @@ -17,7 +17,9 @@
{%- set identifier = model['alias'] -%}
{%- if location_root is not none %}
{%- set model_path = adapter.compute_external_path(config, model, is_incremental()) %}
{%- if model_path != '' -%}
location '{{ model_path }}'
{%- endif -%}
{%- elif (not relation.is_hive_metastore()) and file_format != 'delta' -%}
{{ exceptions.raise_compiler_error(
'Incompatible configuration: `location_root` must be set when using a non-delta file format with Unity Catalog'
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ python = "3.9"
setup-precommit = "pre-commit install"
code-quality = "pre-commit run --all-files"
unit = "pytest --color=yes -v --profile databricks_cluster -n 10 --dist=loadscope tests/unit"
cluster-e2e = "pytest --color=yes -v --profile databricks_cluster -n 10 --dist=loadscope tests/functional"
uc-cluster-e2e = "pytest --color=yes -v --profile databricks_uc_cluster -n 10 --dist=loadscope tests/functional"
sqlw-e2e = "pytest --color=yes -v --profile databricks_uc_sql_endpoint -n 10 --dist=loadscope tests/functional"
cluster-e2e = "pytest --color=yes -v --profile databricks_cluster -n 10 --dist=loadfile tests/functional"
uc-cluster-e2e = "pytest --color=yes -v --profile databricks_uc_cluster -n 10 --dist=loadfile tests/functional"
sqlw-e2e = "pytest --color=yes -v --profile databricks_uc_sql_endpoint -n 10 --dist=loadfile tests/functional"

[tool.hatch.envs.test.scripts]
unit = "pytest --color=yes -v --profile databricks_cluster -n 10 --dist=loadscope tests/unit"
Expand Down
112 changes: 112 additions & 0 deletions tests/functional/adapter/simple_seed/test_seed_no_location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Test that seeds work correctly without specifying location_root.

This test ensures that when no location_root is specified, the adapter
does not write an empty location clause which would cause the error:
"Can not create a Path from an empty string"

See: https://github.com/databricks/dbt-databricks/issues/1228
"""

import pytest
from dbt.tests import util

from tests.functional.adapter.fixtures import MaterializationV2Mixin

# Simple seed CSV data
simple_seed_csv = """id,name,value
1,Alice,100
2,Bob,200
3,Charlie,300
"""


class TestSeedNoLocation:
"""Test seed creation without location_root config."""

@pytest.fixture(scope="class")
def seeds(self):
return {"simple_seed.csv": simple_seed_csv}

@pytest.fixture(scope="class")
def project_config_update(self):
"""Explicitly do not set location_root to test default behavior."""
return {
"seeds": {
# No location_root specified - this is the default case
"+file_format": "delta",
}
}

def test_seed_without_location_root(self, project):
"""Test that seeds can be created without specifying location_root."""
# Run seed command
results = util.run_dbt(["seed"])
assert len(results) == 1
assert results[0].status == "success"

# Verify the seed was created and has the correct data
sql = "select count(*) as num_rows from {schema}.simple_seed"
result = project.run_sql(sql, fetch="one")
assert result[0] == 3

def test_seed_full_refresh_without_location_root(self, project):
"""Test that seed full refresh works without location_root."""
# Initial seed
results = util.run_dbt(["seed"])
assert len(results) == 1

# Full refresh
results = util.run_dbt(["seed", "--full-refresh"])
assert len(results) == 1
assert results[0].status == "success"

# Verify data is still correct
sql = "select count(*) as num_rows from {schema}.simple_seed"
result = project.run_sql(sql, fetch="one")
assert result[0] == 3


class TestSeedNoLocationV2(TestSeedNoLocation, MaterializationV2Mixin):
"""Test seed creation without location_root using v2 materialization."""

pass


class TestSeedEmptyLocationRoot:
"""Test seed creation with explicitly empty location_root.

This tests the edge case where location_root might be set to empty string
rather than None, which could cause the "Can not create a Path from an empty string" error.
"""

@pytest.fixture(scope="class")
def seeds(self):
return {"empty_location_seed.csv": simple_seed_csv}

@pytest.fixture(scope="class")
def project_config_update(self):
"""Test with an empty location_root (edge case)."""
return {
"seeds": {
# Explicitly empty location_root should be handled gracefully
# In practice, dbt might filter this out, but we want to ensure robustness
"+file_format": "delta",
}
}

def test_seed_with_empty_config(self, project):
"""Test that seeds work even if location_root config could be empty."""
results = util.run_dbt(["seed"])
assert len(results) == 1
assert results[0].status == "success"

# Verify the seed was created
sql = "select count(*) as num_rows from {schema}.empty_location_seed"
result = project.run_sql(sql, fetch="one")
assert result[0] == 3


class TestSeedEmptyLocationRootV2(TestSeedEmptyLocationRoot, MaterializationV2Mixin):
"""Test seed creation with empty location_root using v2 materialization."""

pass
2 changes: 2 additions & 0 deletions tests/unit/catalogs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Tests for catalog-related functionality

91 changes: 91 additions & 0 deletions tests/unit/catalogs/test_catalog_relation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from dbt.adapters.databricks.catalogs._relation import DatabricksCatalogRelation


class TestDatabricksCatalogRelation:
"""Test the DatabricksCatalogRelation class, particularly the location property."""

def test_location__both_set(self):
"""Test that location is properly computed when both location_root and location_path are set."""
relation = DatabricksCatalogRelation(
external_volume="s3://my-bucket/path",
location_path="my_table",
)
assert relation.location == "s3://my-bucket/path/my_table"

def test_location__only_root_set(self):
"""Test that location returns None when only location_root is set."""
relation = DatabricksCatalogRelation(
external_volume="s3://my-bucket/path",
location_path=None,
)
assert relation.location is None

def test_location__only_path_set(self):
"""Test that location returns None when only location_path is set."""
relation = DatabricksCatalogRelation(
external_volume=None,
location_path="my_table",
)
assert relation.location is None

def test_location__both_none(self):
"""Test that location returns None when both are None."""
relation = DatabricksCatalogRelation(
external_volume=None,
location_path=None,
)
assert relation.location is None

def test_location__empty_string_root(self):
"""Test that location returns None when location_root is an empty string."""
relation = DatabricksCatalogRelation(
external_volume="",
location_path="my_table",
)
assert relation.location is None

def test_location__empty_string_path(self):
"""Test that location returns None when location_path is an empty string."""
relation = DatabricksCatalogRelation(
external_volume="s3://my-bucket/path",
location_path="",
)
assert relation.location is None

def test_location__both_empty_strings(self):
"""Test that location returns None when both are empty strings."""
relation = DatabricksCatalogRelation(
external_volume="",
location_path="",
)
assert relation.location is None

def test_location__whitespace_root(self):
"""Test that location handles whitespace in location_root."""
relation = DatabricksCatalogRelation(
external_volume=" ",
location_path="my_table",
)
# posixpath.join will handle this, so it should return " /my_table" or similar
# but we expect the location to be truthy (not None)
assert relation.location is not None

def test_location__root_with_full_path(self):
"""Test location with a full database/schema/identifier path."""
relation = DatabricksCatalogRelation(
external_volume="s3://my-bucket",
location_path="my_catalog/my_schema/my_table",
)
assert relation.location == "s3://my-bucket/my_catalog/my_schema/my_table"

def test_location_root_property(self):
"""Test that location_root property correctly accesses external_volume."""
relation = DatabricksCatalogRelation(external_volume="s3://my-bucket")
assert relation.location_root == "s3://my-bucket"

def test_location_root_setter(self):
"""Test that location_root setter correctly sets external_volume."""
relation = DatabricksCatalogRelation()
relation.location_root = "s3://my-bucket"
assert relation.external_volume == "s3://my-bucket"
assert relation.location_root == "s3://my-bucket"
Loading