Skip to content

Commit

Permalink
(fix) generate_database_name macro should be case insensitive (#453)
Browse files Browse the repository at this point in the history
* add macro test

* force lowercase for quoted identifiers

* extend behavior to generate_schema_name
  • Loading branch information
guenp authored Oct 4, 2024
1 parent bbd04cc commit 41794e3
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 8 deletions.
16 changes: 8 additions & 8 deletions dbt/include/duckdb/macros/adapters.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{%- call statement('create_schema') -%}
{% set sql %}
select type from duckdb_databases()
where database_name='{{ relation.database }}'
where lower(database_name)='{{ relation.database | lower }}'
and type='sqlite'
{% endset %}
{% set results = run_query(sql) %}
Expand Down Expand Up @@ -30,7 +30,7 @@
select schema_name
from system.information_schema.schemata
{% if database is not none %}
where catalog_name = '{{ database }}'
where lower(catalog_name) = '{{ database | lower }}'
{% endif %}
{% endset %}
{{ return(run_query(sql)) }}
Expand All @@ -40,8 +40,8 @@
{% set sql -%}
select count(*)
from system.information_schema.schemata
where schema_name = '{{ schema }}'
and catalog_name = '{{ information_schema.database }}'
where lower(schema_name) = '{{ schema | lower }}'
and lower(catalog_name) = '{{ information_schema.database | lower }}'
{%- endset %}
{{ return(run_query(sql)) }}
{% endmacro %}
Expand Down Expand Up @@ -132,10 +132,10 @@ def materialize(df, con):
from system.information_schema.columns
where table_name = '{{ relation.identifier }}'
{% if relation.schema %}
and table_schema = '{{ relation.schema }}'
and lower(table_schema) = '{{ relation.schema | lower }}'
{% endif %}
{% if relation.database %}
and table_catalog = '{{ relation.database }}'
and lower(table_catalog) = '{{ relation.database | lower }}'
{% endif %}
order by ordinal_position

Expand All @@ -156,8 +156,8 @@ def materialize(df, con):
WHEN 'LOCAL TEMPORARY' THEN 'table'
END as type
from system.information_schema.tables
where table_schema = '{{ schema_relation.schema }}'
and table_catalog = '{{ schema_relation.database }}'
where lower(table_schema) = '{{ schema_relation.schema | lower }}'
and lower(table_catalog) = '{{ schema_relation.database | lower }}'
{% endcall %}
{{ return(load_result('list_relations_without_caching').table) }}
{% endmacro %}
Expand Down
34 changes: 34 additions & 0 deletions tests/functional/plugins/motherduck/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#
# Models
#

models__gen_data_macro = """
select * from {{ ref("seed") }}
"""

#
# Macros
#

macros__generate_database_name = """
{% macro generate_database_name(custom_database_name=none, node=none) -%}
{{ target.database | trim }}_{{ var("build_env") | trim }}_{{ var("org_prefix") | trim }}
{%- endmacro %}
"""


macros__generate_schema_name = """
{% macro generate_schema_name(custom_schema_name=none, node=none) -%}
{{ target.schema | trim }}_{{ var("build_env") | trim }}_{{ var("org_prefix") | trim }}
{%- endmacro %}
"""

#
# Seeds
#

seeds__example_seed_csv = """a,b,c
1,2,3
4,5,6
7,8,9
"""
115 changes: 115 additions & 0 deletions tests/functional/plugins/motherduck/test_macros.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
Test that the generate database name macro is case insensitive
See DuckDB docs: https://duckdb.org/docs/sql/dialect/keywords_and_identifiers.html
"Identifiers in DuckDB are always case-insensitive, similarly to PostgreSQL.
However, unlike PostgreSQL (and some other major SQL implementations), DuckDB also
treats quoted identifiers as case-insensitive."
"""
from urllib.parse import urlparse
import pytest

from dbt.tests.util import (
run_dbt,
check_result_nodes_by_name
)
from tests.functional.plugins.motherduck.fixtures import (
models__gen_data_macro,
macros__generate_database_name,
macros__generate_schema_name,
seeds__example_seed_csv,
)


@pytest.mark.skip_profile("buenavista", "file", "memory")
class TestMacrosGenerateDatabaseName:
@pytest.fixture(scope="class")
def database_name(self, dbt_profile_target, request):
return urlparse(dbt_profile_target["path"]).path + "_ducky_ducky"

@pytest.fixture(autouse=True)
def run_dbt_scope(self, project, database_name):
project.run_sql(f"CREATE DATABASE IF NOT EXISTS {database_name}")
yield
project.run_sql(f"DROP DATABASE {database_name}")

@pytest.fixture(scope="class")
def seeds(self):
return {
"seed.csv": seeds__example_seed_csv,
}

@pytest.fixture(scope="class")
def models(self):
return {
"model.sql": models__gen_data_macro
}

@pytest.fixture(scope="class")
def macros(self):
return {
"db_name.sql": macros__generate_database_name,
"schema_name.sql": macros__generate_schema_name
}

@staticmethod
def gen_project_config_update(build_env, org_prefix):
return {
"config-version": 2,
"vars": {
"test": {
"build_env": build_env,
"org_prefix": org_prefix
},
},
"macro-paths": ["macros"],
}

@pytest.fixture(scope="class")
def project_config_update(self):
return self.gen_project_config_update("ducky", "ducky")

def test_dbname_macro(self, project):
# seed command
results = run_dbt(["seed"])
assert len(results) == 1
check_result_nodes_by_name(results, ["seed"])

for _ in range(3):
results = run_dbt(["run"])
assert len(results) == 1
check_result_nodes_by_name(results, ["model"])


@pytest.mark.skip_profile("buenavista", "file", "memory")
class TestMacrosGenerateDatabaseNameUpperCase(TestMacrosGenerateDatabaseName):
@pytest.fixture(scope="class")
def database_name(self, dbt_profile_target, request):
return urlparse(dbt_profile_target["path"]).path + "_ducky_ducky"

@pytest.fixture(scope="class")
def project_config_update(self):
return self.gen_project_config_update("DUCKY", "DUCKY")


@pytest.mark.skip_profile("buenavista", "file", "memory")
class TestMacrosGenerateDatabaseNameLowerCase(TestMacrosGenerateDatabaseName):
@pytest.fixture(scope="class")
def database_name(self, dbt_profile_target, request):
return urlparse(dbt_profile_target["path"]).path + "_DUCKY_DUCKY"

@pytest.fixture(scope="class")
def project_config_update(self):
return self.gen_project_config_update("ducky", "ducky")


@pytest.mark.skip_profile("buenavista", "file", "memory")
class TestMacrosGenerateDatabaseNameAllMixedCase(TestMacrosGenerateDatabaseName):
@pytest.fixture(scope="class")
def database_name(self, dbt_profile_target, request):
return urlparse(dbt_profile_target["path"]).path + "_dUcKy_DUckY"

@pytest.fixture(scope="class")
def project_config_update(self):
return self.gen_project_config_update("DuCkY", "dUcKy")

0 comments on commit 41794e3

Please sign in to comment.