Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for persist_docs-related functionality #367

Merged
merged 5 commits into from
Mar 27, 2024
Merged
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
9 changes: 9 additions & 0 deletions dbt/adapters/duckdb/environments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ def __init__(self, creds: DuckDBCredentials):
if path not in sys.path:
sys.path.append(path)

major, minor, patch = [int(x) for x in duckdb.__version__.split(".")]
if major == 0 and (minor < 10 or (minor == 10 and patch == 0)):
self._supports_comments = False
else:
self._supports_comments = True

@property
def creds(self) -> DuckDBCredentials:
return self._creds
Expand All @@ -109,6 +115,9 @@ def store_relation(self, plugin_name: str, target_config: TargetConfig) -> None:
def get_binding_char(self) -> str:
return "?"

def supports_comments(self) -> bool:
return self._supports_comments

@classmethod
def initialize_db(
cls, creds: DuckDBCredentials, plugins: Optional[Dict[str, BasePlugin]] = None
Expand Down
7 changes: 7 additions & 0 deletions dbt/adapters/duckdb/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ def external_root(self) -> str:
def get_binding_char(self):
return DuckDBConnectionManager.env().get_binding_char()

@available
def catalog_comment(self, prefix):
if DuckDBConnectionManager.env().supports_comments():
return f"{prefix}.comment"
else:
return "''"

@available
def external_write_options(self, write_location: str, rendered_options: dict) -> str:
if "format" not in rendered_options:
Expand Down
43 changes: 30 additions & 13 deletions dbt/include/duckdb/macros/catalog.sql
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@

{% macro duckdb__get_catalog(information_schema, schemas) -%}
{%- call statement('catalog', fetch_result=True) -%}
with relations AS (
select
t.table_name
, t.database_name
, t.schema_name
, 'BASE TABLE' as table_type
, {{ adapter.catalog_comment('t') }} as table_comment
from duckdb_tables() t
WHERE t.database_name = '{{ database }}'
UNION ALL
SELECT v.view_name as table_name
, v.database_name
, v.schema_name
, 'VIEW' as table_type
, {{ adapter.catalog_comment('v') }} as table_comment
from duckdb_views() v
WHERE v.database_name = '{{ database }}'
)
select
'{{ database }}' as table_database,
t.table_schema,
t.table_name,
t.table_type,
'' as table_comment,
r.schema_name as table_schema,
r.table_name,
r.table_type,
r.table_comment,
c.column_name,
c.ordinal_position as column_index,
c.data_type column_type,
'' as column_comment,
c.column_index as column_index,
c.data_type as column_type,
{{ adapter.catalog_comment('c') }} as column_comment,
'' as table_owner
FROM information_schema.tables t JOIN information_schema.columns c ON t.table_schema = c.table_schema AND t.table_name = c.table_name
FROM relations r JOIN duckdb_columns() c ON r.schema_name = c.schema_name AND r.table_name = c.table_name
WHERE (
{%- for schema in schemas -%}
upper(t.table_schema) = upper('{{ schema }}'){%- if not loop.last %} or {% endif -%}
upper(r.schema_name) = upper('{{ schema }}'){%- if not loop.last %} or {% endif -%}
{%- endfor -%}
)
AND t.table_type IN ('BASE TABLE', 'VIEW')
ORDER BY
t.table_schema,
t.table_name,
c.ordinal_position
r.schema_name,
r.table_name,
c.column_index
{%- endcall -%}
{{ return(load_result('catalog').table) }}
{%- endmacro %}
36 changes: 36 additions & 0 deletions dbt/include/duckdb/macros/persist_docs.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

{#
The logic in this file is adapted from dbt-postgres, since DuckDB matches
the Postgres relation/column commenting model as of 0.10.1
#}

{#
By using dollar-quoting like this, users can embed anything they want into their comments
(including nested dollar-quoting), as long as they do not use this exact dollar-quoting
label. It would be nice to just pick a new one but eventually you do have to give up.
#}
{% macro duckdb_escape_comment(comment) -%}
{% if comment is not string %}
{% do exceptions.raise_compiler_error('cannot escape a non-string: ' ~ comment) %}
{% endif %}
{%- set magic = '$dbt_comment_literal_block$' -%}
{%- if magic in comment -%}
{%- do exceptions.raise_compiler_error('The string ' ~ magic ~ ' is not allowed in comments.') -%}
{%- endif -%}
{{ magic }}{{ comment }}{{ magic }}
{%- endmacro %}

{% macro duckdb__alter_relation_comment(relation, comment) %}
{% set escaped_comment = duckdb_escape_comment(comment) %}
comment on {{ relation.type }} {{ relation }} is {{ escaped_comment }};
{% endmacro %}


{% macro duckdb__alter_column_comment(relation, column_dict) %}
{% set existing_columns = adapter.get_columns_in_relation(relation) | map(attribute="name") | list %}
{% for column_name in column_dict if (column_name in existing_columns) %}
{% set comment = column_dict[column_name]['description'] %}
{% set escaped_comment = duckdb_escape_comment(comment) %}
comment on column {{ relation }}.{{ adapter.quote(column_name) if column_dict[column_name]['quote'] else column_name }} is {{ escaped_comment }};
{% endfor %}
{% endmacro %}
21 changes: 21 additions & 0 deletions tests/functional/adapter/test_persist_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pytest

from dbt.tests.adapter.persist_docs.test_persist_docs import (
BasePersistDocs,
BasePersistDocsColumnMissing,
BasePersistDocsCommentOnQuotedColumn,
)

@pytest.mark.skip_profile("md")
class TestPersistDocs(BasePersistDocs):
pass


@pytest.mark.skip_profile("md")
class TestPersistDocsColumnMissing(BasePersistDocsColumnMissing):
pass


@pytest.mark.skip_profile("md")
class TestPersistDocsCommentOnQuotedColumn(BasePersistDocsCommentOnQuotedColumn):
pass
1 change: 1 addition & 0 deletions tests/unit/test_duckdb_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def adapter(self):

@mock.patch("dbt.adapters.duckdb.environments.duckdb")
def test_acquire_connection(self, connector):
connector.__version__ = "0.1.0" # dummy placeholder for semver checks
DuckDBConnectionManager.close_all_connections()
connection = self.adapter.acquire_connection("dummy")

Expand Down
Loading