Skip to content

Commit 83bb169

Browse files
committed
feat: widen dbt-core compatibility range
1 parent 0b819d2 commit 83bb169

File tree

15 files changed

+218
-73
lines changed

15 files changed

+218
-73
lines changed

.github/workflows/pr.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ on:
88
concurrency:
99
group: 'pr-${{ github.event.pull_request.number }}'
1010
cancel-in-progress: true
11+
permissions:
12+
contents: read
1113
jobs:
1214
test-vscode:
1315
env:
@@ -66,3 +68,52 @@ jobs:
6668
name: playwright-report
6769
path: vscode/extension/playwright-report/
6870
retention-days: 30
71+
test-dbt-versions:
72+
runs-on: ubuntu-latest
73+
strategy:
74+
fail-fast: false
75+
matrix:
76+
dbt-version: ["1.3.0", "1.4.0", "1.5.0", "1.6.0", "1.7.0", "1.8.0", "1.9.0", "1.10.0"]
77+
steps:
78+
- uses: actions/checkout@v5
79+
- name: Set up Python
80+
uses: actions/setup-python@v5
81+
with:
82+
python-version: '3.10'
83+
- name: Install uv
84+
uses: astral-sh/setup-uv@v6
85+
- name: Install SQLMesh dev dependencies
86+
run: |
87+
uv venv .venv
88+
source .venv/bin/activate
89+
sed -i 's/"pydantic>=2.0.0"/"pydantic"/g' pyproject.toml
90+
if [[ "${{ matrix.dbt-version }}" == "1.10.0" ]]; then
91+
# For 1.10.0: only add version to dbt-core, remove versions from all adapter packages
92+
sed -i -E 's/"(dbt-core)[^"]*"/"\1~=${{ matrix.dbt-version }}"/g' pyproject.toml
93+
# Remove version constraints from all dbt adapter packages
94+
sed -i -E 's/"(dbt-(bigquery|duckdb|snowflake|athena-community|clickhouse|databricks|redshift|trino))[^"]*"/"\1"/g' pyproject.toml
95+
else
96+
# For other versions: apply version to all dbt packages
97+
sed -i -E 's/"(dbt-[^">=<~!]+)[^"]*"/"\1~=${{ matrix.dbt-version }}"/g' pyproject.toml
98+
fi
99+
UV=1 make install-dev
100+
uv pip install pydantic>=2.0.0 --reinstall
101+
- name: Run dbt tests
102+
# We can't run slow tests across all engines due to tests requiring DuckDB and old versions
103+
# of DuckDB require a version of DuckDB we no longer support
104+
run: |
105+
source .venv/bin/activate
106+
make dbt-fast-test
107+
# - name: Remove versions sections for dbt < 1.5.0
108+
# run: |
109+
# if [[ "${{ matrix.dbt-version }}" < "1.5.0" ]]; then
110+
# echo "DBT version is ${{ matrix.dbt-version }}, removing versions sections..."
111+
# sed -i '/^ versions:/,/^[^ ]/{ /^ versions:/d; /^ - v:/d; }' examples/sushi_dbt/models/schema.yml
112+
# else
113+
# echo "DBT version is ${{ matrix.dbt-version }}, keeping versions sections"
114+
# fi
115+
- name: Test SQLMesh info in sushi_dbt
116+
working-directory: ./examples/sushi_dbt
117+
run: |
118+
source ../../.venv/bin/activate
119+
sqlmesh info --skip-connection

Makefile

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
.PHONY: docs
22

3+
ifdef UV
4+
PIP := uv pip
5+
else
6+
PIP := pip3
7+
endif
8+
39
install-dev:
4-
pip3 install -e ".[dev,web,slack,dlt,lsp]" ./examples/custom_materializations
10+
$(PIP) install -e ".[dev,web,slack,dlt,lsp]" ./examples/custom_materializations
511

612
install-doc:
7-
pip3 install -r ./docs/requirements.txt
13+
$(PIP) install -r ./docs/requirements.txt
814

915
install-pre-commit:
1016
pre-commit install
@@ -22,16 +28,16 @@ doc-test:
2228
python -m pytest --doctest-modules sqlmesh/core sqlmesh/utils
2329

2430
package:
25-
pip3 install build && python3 -m build
31+
$(PIP) install build && python3 -m build
2632

2733
publish: package
28-
pip3 install twine && python3 -m twine upload dist/*
34+
$(PIP) install twine && python3 -m twine upload dist/*
2935

3036
package-tests:
31-
pip3 install build && cp pyproject.toml tests/sqlmesh_pyproject.toml && python3 -m build tests/
37+
$(PIP) install build && cp pyproject.toml tests/sqlmesh_pyproject.toml && python3 -m build tests/
3238

3339
publish-tests: package-tests
34-
pip3 install twine && python3 -m twine upload -r tobiko-private tests/dist/*
40+
$(PIP) install twine && python3 -m twine upload -r tobiko-private tests/dist/*
3541

3642
docs-serve:
3743
mkdocs serve
@@ -93,6 +99,9 @@ engine-test:
9399
dbt-test:
94100
pytest -n auto -m "dbt and not cicdonly"
95101

102+
dbt-fast-test:
103+
pytest -n auto -m "dbt and fast"
104+
96105
github-test:
97106
pytest -n auto -m "github"
98107

@@ -109,7 +118,7 @@ guard-%:
109118
fi
110119

111120
engine-%-install:
112-
pip3 install -e ".[dev,web,slack,lsp,${*}]" ./examples/custom_materializations
121+
$(PIP) install -e ".[dev,web,slack,lsp,${*}]" ./examples/custom_materializations
113122

114123
engine-docker-%-up:
115124
docker compose -f ./tests/core/engine_adapter/integration/docker/compose.${*}.yaml up -d
@@ -157,11 +166,11 @@ snowflake-test: guard-SNOWFLAKE_ACCOUNT guard-SNOWFLAKE_WAREHOUSE guard-SNOWFLAK
157166
pytest -n auto -m "snowflake" --retries 3 --junitxml=test-results/junit-snowflake.xml
158167

159168
bigquery-test: guard-BIGQUERY_KEYFILE engine-bigquery-install
160-
pip install -e ".[bigframes]"
169+
$(PIP) install -e ".[bigframes]"
161170
pytest -n auto -m "bigquery" --retries 3 --junitxml=test-results/junit-bigquery.xml
162171

163172
databricks-test: guard-DATABRICKS_CATALOG guard-DATABRICKS_SERVER_HOSTNAME guard-DATABRICKS_HTTP_PATH guard-DATABRICKS_ACCESS_TOKEN guard-DATABRICKS_CONNECT_VERSION engine-databricks-install
164-
pip install 'databricks-connect==${DATABRICKS_CONNECT_VERSION}'
173+
$(PIP) install 'databricks-connect==${DATABRICKS_CONNECT_VERSION}'
165174
pytest -n auto -m "databricks" --retries 3 --junitxml=test-results/junit-databricks.xml
166175

167176
redshift-test: guard-REDSHIFT_HOST guard-REDSHIFT_USER guard-REDSHIFT_PASSWORD guard-REDSHIFT_DATABASE engine-redshift-install

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ bigframes = ["bigframes>=1.32.0"]
5252
clickhouse = ["clickhouse-connect"]
5353
databricks = ["databricks-sql-connector[pyarrow]"]
5454
dev = [
55-
"agate==1.7.1",
55+
"agate",
5656
"beautifulsoup4",
5757
"clickhouse-connect",
5858
"cryptography",

sqlmesh/dbt/loader.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,11 @@ def _load_projects(self) -> t.List[Project]:
188188

189189
self._projects.append(project)
190190

191-
if project.context.target.database != (self.context.default_catalog or ""):
192-
raise ConfigError("Project default catalog does not match context default catalog")
191+
context_default_catalog = self.context.default_catalog or ""
192+
if project.context.target.database != context_default_catalog:
193+
raise ConfigError(
194+
f"Project default catalog ('{project.context.target.database}') does not match context default catalog ('{context_default_catalog}')."
195+
)
193196
for path in project.project_files:
194197
self._track_file(path)
195198

sqlmesh/dbt/manifest.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
from sqlmesh.utils.conversions import make_serializable
1616

1717
# Override the file name to prevent dbt commands from invalidating the cache.
18-
dbt_constants.PARTIAL_PARSE_FILE_NAME = "sqlmesh_partial_parse.msgpack"
18+
if hasattr(dbt_constants, "PARTIAL_PARSE_FILE_NAME"):
19+
dbt_constants.PARTIAL_PARSE_FILE_NAME = "sqlmesh_partial_parse.msgpack"
20+
else:
21+
from dbt.parser import manifest as dbt_manifest
22+
23+
dbt_manifest.PARTIAL_PARSE_FILE_NAME = "sqlmesh_partial_parse.msgpack"
1924

2025
import jinja2
2126
from dbt.adapters.factory import register_adapter, reset_adapters
@@ -378,11 +383,17 @@ def _load_on_run_start_end(self) -> None:
378383

379384
if "on-run-start" in node.tags:
380385
self._on_run_start_per_package[node.package_name][node_name] = HookConfig(
381-
sql=sql, index=node.index or 0, path=node_path, dependencies=dependencies
386+
sql=sql,
387+
index=getattr(node, "index", None) or 0,
388+
path=node_path,
389+
dependencies=dependencies,
382390
)
383391
else:
384392
self._on_run_end_per_package[node.package_name][node_name] = HookConfig(
385-
sql=sql, index=node.index or 0, path=node_path, dependencies=dependencies
393+
sql=sql,
394+
index=getattr(node, "index", None) or 0,
395+
path=node_path,
396+
dependencies=dependencies,
386397
)
387398

388399
@property
@@ -598,6 +609,9 @@ def _macro_references(
598609
manifest: Manifest, node: t.Union[ManifestNode, Macro]
599610
) -> t.Set[MacroReference]:
600611
result: t.Set[MacroReference] = set()
612+
if not hasattr(node, "depends_on"):
613+
return result
614+
601615
for macro_node_id in node.depends_on.macros:
602616
if not macro_node_id:
603617
continue
@@ -613,18 +627,20 @@ def _macro_references(
613627

614628
def _refs(node: ManifestNode) -> t.Set[str]:
615629
if DBT_VERSION >= (1, 5, 0):
616-
result = set()
630+
result: t.Set[str] = set()
631+
if not hasattr(node, "refs"):
632+
return result
617633
for r in node.refs:
618-
ref_name = f"{r.package}.{r.name}" if r.package else r.name
634+
ref_name = f"{r.package}.{r.name}" if r.package else r.name # type: ignore
619635
if getattr(r, "version", None):
620-
ref_name = f"{ref_name}_v{r.version}"
636+
ref_name = f"{ref_name}_v{r.version}" # type: ignore
621637
result.add(ref_name)
622638
return result
623639
return {".".join(r) for r in node.refs} # type: ignore
624640

625641

626642
def _sources(node: ManifestNode) -> t.Set[str]:
627-
return {".".join(s) for s in node.sources}
643+
return {".".join(s) for s in getattr(node, "sources", [])}
628644

629645

630646
def _model_node_id(model_name: str, package: str) -> str:

sqlmesh/dbt/relation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from sqlmesh.dbt.util import DBT_VERSION
22

33

4-
if DBT_VERSION < (1, 8, 0):
5-
from dbt.contracts.relation import * # type: ignore # noqa: F403
6-
else:
4+
if DBT_VERSION >= (1, 8, 0):
75
from dbt.adapters.contracts.relation import * # type: ignore # noqa: F403
6+
else:
7+
from dbt.contracts.relation import * # type: ignore # noqa: F403

sqlmesh/dbt/seed.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
import agate
77

8-
try:
8+
from sqlmesh.dbt.util import DBT_VERSION
9+
10+
if DBT_VERSION >= (1, 8, 0):
911
from dbt_common.clients import agate_helper # type: ignore
1012

1113
SUPPORTS_DELIMITER = True
12-
except ImportError:
14+
else:
1315
from dbt.clients import agate_helper # type: ignore
1416

1517
SUPPORTS_DELIMITER = False
@@ -90,31 +92,33 @@ def to_sqlmesh(
9092
)
9193

9294

93-
class Integer(agate_helper.Integer):
94-
def cast(self, d: t.Any) -> t.Optional[int]:
95-
if isinstance(d, str):
96-
# The dbt's implementation doesn't support coercion of strings to integers.
97-
if d.strip().lower() in self.null_values:
98-
return None
99-
try:
100-
return int(d)
101-
except ValueError:
102-
raise agate.exceptions.CastError('Can not parse value "%s" as Integer.' % d)
103-
return super().cast(d)
104-
105-
def jsonify(self, d: t.Any) -> str:
106-
return d
107-
108-
109-
agate_helper.Integer = Integer # type: ignore
110-
111-
11295
AGATE_TYPE_MAPPING = {
113-
agate_helper.Integer: exp.DataType.build("int"),
11496
agate_helper.Number: exp.DataType.build("double"),
11597
agate_helper.ISODateTime: exp.DataType.build("datetime"),
11698
agate.Date: exp.DataType.build("date"),
11799
agate.DateTime: exp.DataType.build("datetime"),
118100
agate.Boolean: exp.DataType.build("boolean"),
119101
agate.Text: exp.DataType.build("text"),
120102
}
103+
104+
105+
if DBT_VERSION >= (1, 7, 0):
106+
107+
class Integer(agate_helper.Integer):
108+
def cast(self, d: t.Any) -> t.Optional[int]:
109+
if isinstance(d, str):
110+
# The dbt's implementation doesn't support coercion of strings to integers.
111+
if d.strip().lower() in self.null_values:
112+
return None
113+
try:
114+
return int(d)
115+
except ValueError:
116+
raise agate.exceptions.CastError('Can not parse value "%s" as Integer.' % d)
117+
return super().cast(d)
118+
119+
def jsonify(self, d: t.Any) -> str:
120+
return d
121+
122+
agate_helper.Integer = Integer # type: ignore
123+
124+
AGATE_TYPE_MAPPING[agate_helper.Integer] = exp.DataType.build("int")

sqlmesh/dbt/util.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ def _get_dbt_version() -> t.Tuple[int, int, int]:
2020

2121
DBT_VERSION = _get_dbt_version()
2222

23-
if DBT_VERSION < (1, 8, 0):
24-
from dbt.clients.agate_helper import table_from_data_flat, empty_table, as_matrix # type: ignore # noqa: F401
25-
else:
23+
if DBT_VERSION >= (1, 8, 0):
2624
from dbt_common.clients.agate_helper import table_from_data_flat, empty_table, as_matrix # type: ignore # noqa: F401
25+
else:
26+
from dbt.clients.agate_helper import table_from_data_flat, empty_table, as_matrix # type: ignore # noqa: F401
2727

2828

2929
def pandas_to_agate(df: pd.DataFrame) -> agate.Table:

tests/dbt/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from sqlmesh.core.context import Context
88
from sqlmesh.dbt.context import DbtContext
99
from sqlmesh.dbt.project import Project
10+
from sqlmesh.dbt.target import PostgresConfig
1011

1112

1213
@pytest.fixture()
@@ -25,3 +26,16 @@ def render(value: str) -> str:
2526
return render
2627

2728
return create_renderer
29+
30+
31+
@pytest.fixture()
32+
def dbt_dummy_postgres_config() -> PostgresConfig:
33+
return PostgresConfig( # type: ignore
34+
name="postgres",
35+
host="host",
36+
user="user",
37+
password="password",
38+
dbname="dbname",
39+
port=5432,
40+
schema="schema",
41+
)

0 commit comments

Comments
 (0)