Repository navigation
fix(duckdb): use regular tables instead of temp tables under dbt Fusion #1061
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
base: master
Are you sure you want to change the base?
Changes from all commits
1f2ac7b
65d35be
02378be
65d4612
0f8ac4f
f7256ea
736e38b
7a2d19f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import os | ||
| import shutil | ||
| from pathlib import Path | ||
| from tempfile import mkdtemp | ||
|
|
@@ -6,7 +7,12 @@ | |
| import pytest | ||
| import yaml | ||
| from dbt.version import __version__ as dbt_version | ||
| from dbt_project import PYTEST_XDIST_WORKER, SCHEMA_NAME_SUFFIX, DbtProject | ||
| from dbt_project import ( | ||
| DBT2_RUNNERS, | ||
| PYTEST_XDIST_WORKER, | ||
| SCHEMA_NAME_SUFFIX, | ||
| DbtProject, | ||
| ) | ||
| from elementary.clients.dbt.factory import RunnerMethod | ||
| from env import Environment | ||
| from logger import get_logger | ||
|
|
@@ -18,6 +24,7 @@ | |
| "bigquery", | ||
| "redshift", | ||
| "databricks_catalog", | ||
| "duckdb", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The duckdb profile is still
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gated, and it fails fast rather than skipping: the fixture reads the resolved It fires only on all three of a dbt 2.0
|
||
| ] | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
@@ -70,7 +77,7 @@ def _edit_packages_yml_to_include_absolute_elementary_package_path( | |
| def _remove_python_models_for_dbt_fusion( | ||
| project_dir_copy: str, runner_method: Optional[RunnerMethod] | ||
| ): | ||
| if runner_method != RunnerMethod.FUSION: | ||
| if runner_method not in DBT2_RUNNERS: | ||
| return | ||
|
|
||
| logger.info(f"Removing python tests for project {project_dir_copy}") | ||
|
|
@@ -129,7 +136,7 @@ def only_on_targets(request, target: str): | |
| @pytest.fixture(autouse=True) | ||
| def skip_for_dbt_fusion(request, runner_method: Optional[RunnerMethod]): | ||
| if request.node.get_closest_marker("skip_for_dbt_fusion"): | ||
| if runner_method == RunnerMethod.FUSION: | ||
| if runner_method in DBT2_RUNNERS: | ||
| pytest.skip("Test unsupported for dbt fusion") | ||
|
|
||
|
|
||
|
|
@@ -169,16 +176,60 @@ def clear_on_end(request) -> bool: | |
| return request.config.getoption("--clear-on-end") | ||
|
|
||
|
|
||
| def _profile_path(target: str) -> Optional[str]: | ||
| """Return the ``path`` dbt resolves for *target*, if it can be resolved. | ||
|
|
||
| Goes through dbt's own profile handling instead of reading ``profiles.yml`` | ||
| directly, so ``env_var`` and any other Jinja in ``path`` render the way dbt | ||
| will render it, and an omitted ``path`` picks up dbt-duckdb's ``:memory:`` | ||
| default. ``env_var`` needs the invocation context, hence the | ||
| ``set_invocation_context`` call. | ||
|
|
||
| Returns ``None`` when the profile cannot be resolved, so callers only act on | ||
| a value they positively read. | ||
| """ | ||
| from argparse import Namespace | ||
|
|
||
| from dbt.config.runtime import RuntimeConfig | ||
| from dbt.flags import set_from_args | ||
| from dbt_common.context import set_invocation_context | ||
|
|
||
| profiles_dir = os.environ.get("DBT_PROFILES_DIR", os.path.expanduser("~/.dbt")) | ||
| args = Namespace( | ||
| project_dir=str(DBT_PROJECT_PATH), | ||
| profiles_dir=profiles_dir, | ||
| target=target, | ||
| threads=1, | ||
| vars={}, | ||
| profile=None, | ||
| PROFILES_DIR=profiles_dir, | ||
| PROJECT_DIR=str(DBT_PROJECT_PATH), | ||
| ) | ||
| try: | ||
| set_invocation_context(os.environ) | ||
| set_from_args(args, None) | ||
| return getattr(RuntimeConfig.from_args(args).credentials, "path", None) | ||
| except Exception: | ||
| return None | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def runner_method(request, target: str) -> Optional[RunnerMethod]: | ||
| runner_method_str = request.config.getoption("--runner-method") | ||
| if runner_method_str: | ||
| runner_method = RunnerMethod(runner_method_str) | ||
| if runner_method in DBT2_RUNNERS and target not in DBT_FUSION_SUPPORTED_TARGETS: | ||
| raise ValueError(f"Fusion runner is not supported for target: {target}") | ||
| if ( | ||
| runner_method == RunnerMethod.FUSION | ||
| and target not in DBT_FUSION_SUPPORTED_TARGETS | ||
| runner_method in DBT2_RUNNERS | ||
| and target == "duckdb" | ||
| and _profile_path(target) == ":memory:" | ||
| ): | ||
| raise ValueError(f"Fusion runner is not supported for target: {target}") | ||
| raise ValueError( | ||
| f"The {runner_method.value} runner runs dbt in a subprocess, which " | ||
| "cannot share an in-memory DuckDB database with the tests. Set " | ||
| "`path` to a file in the duckdb target of your profiles.yml." | ||
| ) | ||
| return runner_method | ||
| return None | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,11 +25,13 @@ | |
| {% endmacro %} | ||
|
|
||
|
|
||
| {# Session-scoped temp tables are invisible to the next statement under dbt-fusion, | ||
| which runs each statement on a pooled connection ("relation does not exist"). | ||
| These adapters fall back to regular tables with cleanup instead. #} | ||
| {% macro redshift__has_temp_table_support() %} | ||
| {# dbt-fusion uses connection pooling, so temp tables created in one session | ||
| are not visible in other sessions, causing "relation does not exist" errors. | ||
| Use regular tables with cleanup instead. #} | ||
| {% if elementary.is_dbt_fusion() %} {% do return(false) %} | ||
| {% else %} {% do return(true) %} | ||
| {% endif %} | ||
| {% do return(not elementary.is_dbt_fusion()) %} | ||
| {% endmacro %} | ||
|
|
||
| {% macro duckdb__has_temp_table_support() %} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the root cause is Fusion-wide, consider one gate (e.g.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, and I'd like it as its own PR. You're right about Postgres: |
||
| {% do return(not elementary.is_dbt_fusion()) %} | ||
| {% endmacro %} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,20 +72,41 @@ | |
| {{ return(elementary.spark__edr_make_temp_relation(base_relation, suffix)) }} | ||
| {% endmacro %} | ||
|
|
||
| {% macro _fusion_regular_temp_relation(base_relation, suffix) %} | ||
| {# dbt-fusion runs each statement on a pooled connection, so a session-scoped temp | ||
| relation is not visible to the statement that reads it. Build a regular relation | ||
| with an explicit database/schema instead - Elementary's normal cleanup drops it. #} | ||
| {% set tmp_identifier = elementary.table_name_with_suffix( | ||
| base_relation.identifier, suffix | ||
| ) %} | ||
| {% do return( | ||
| api.Relation.create( | ||
| identifier=tmp_identifier, | ||
| schema=base_relation.schema, | ||
| database=base_relation.database, | ||
| type="table", | ||
| ) | ||
| ) %} | ||
| {% endmacro %} | ||
|
|
||
| {% macro redshift__edr_make_temp_relation(base_relation, suffix) %} | ||
| {% if elementary.is_dbt_fusion() %} | ||
| {# Workaround for dbt-fusion temp table metadata bug - create regular relations | ||
| with explicit schema/database instead of temp relations #} | ||
| {% set tmp_identifier = elementary.table_name_with_suffix( | ||
| base_relation.identifier, suffix | ||
| {% do return( | ||
| elementary._fusion_regular_temp_relation(base_relation, suffix) | ||
| ) %} | ||
| {% set tmp_relation = api.Relation.create( | ||
| identifier=tmp_identifier, | ||
| schema=base_relation.schema, | ||
| database=base_relation.database, | ||
| type="table", | ||
| {% else %} {% do return(dbt.make_temp_relation(base_relation, suffix)) %} | ||
| {% endif %} | ||
| {% endmacro %} | ||
|
|
||
| {% macro duckdb__edr_make_temp_relation(base_relation, suffix) %} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Identical to
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deduplicated the relation builder; the predicate I left alone.
|
||
| {% if elementary.is_dbt_fusion() %} | ||
| {# DuckDB additionally rejects TEMPORARY on a qualified name, so the regular | ||
| relation is required rather than merely convenient. #} | ||
| {% do return( | ||
| elementary._fusion_regular_temp_relation(base_relation, suffix) | ||
| ) %} | ||
| {% do return(tmp_relation) %} | ||
| {% else %} {% do return(dbt.make_temp_relation(base_relation, suffix)) %} | ||
| {% endif %} | ||
| {% endmacro %} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This tears down and rebuilds the DuckDB env after every query for any file-backed profile, even when no subprocess needs the lock. Gate on runner method (fusion/subprocess), or release once before spawning dbt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
AdapterQueryRunnernow takes the runner method and the whole release is a no-opunless dbt actually runs out of process:
With the API runner dbt shares our process and the same
_ENV, so there is no lock tohand over — which is also why the existing DuckDB CI job is fine with
:memory:. Ididn't go with "release once before invoking dbt" because the tests invoke dbt from
several call sites (
test(),run_operation(), the seeders), so the release point wouldhave to be repeated at each of them.