Compare commits

..

9 Commits

Author SHA1 Message Date
github-actions[bot]
cbd4570c67 Bumping version to 1.1.0 (#5186)
* Bumping version to 1.1.0

* Updating changelog

Co-authored-by: Github Build Bot <buildbot@fishtownanalytics.com>
Co-authored-by: Leah Antkiewicz <leah.antkiewicz@fishtownanalytics.com>
2022-04-28 12:54:30 -04:00
github-actions[bot]
5ae33b90bf Bumping version to 1.1.0rc3 (#5166)
* Bumping version to 1.1.0rc3

* Update changelog

* Fixing spacing issue

* Removing more whitespace

Co-authored-by: Github Build Bot <buildbot@fishtownanalytics.com>
Co-authored-by: Leah Antkiewicz <leah.antkiewicz@fishtownanalytics.com>
Co-authored-by: leahwicz <60146280+leahwicz@users.noreply.github.com>
2022-04-26 15:25:39 -04:00
github-actions[bot]
37d78338c2 [Backport 1.1.latest] Use yaml renderer (with target context) for rendering selectors (#5161)
* Use yaml renderer (with target context) for rendering selectors (#5136)

* Use yaml renderer (with target context) for rendering selectors

* Changie

* Convert cli_vars tests

* Add test for var in profiles

* Add test for cli vars in packages

* Add test for vars in selectors

(cherry picked from commit 1f898c859a)

* Tweak cli_vars_in_packages test to do a run and build an adapter

Co-authored-by: Gerda Shank <gerda@dbtlabs.com>
2022-04-26 14:13:16 -04:00
github-actions[bot]
698420b420 Even more scrubbing (#5152) (#5156)
* Even more scrubbing

* Changelog entry

* Even more

* remove reduendent scrub

* remove reduendent scrub

* fix encoding issue

* keep scrubbed log in args

Co-authored-by: Chenyu Li <chenyu.li@dbtlabs.com>
(cherry picked from commit ce0bcc08a6)

Co-authored-by: Jeremy Cohen <jeremy@dbtlabs.com>
2022-04-26 10:59:07 -06:00
Emily Rockman
e23f0e0747 fix retry logic failures (#5137) (#5148)
* fix retry logic failures

* changelog

* add tests to make sure data is getting where it needs to

* rename file

* remove duplicate file

(cherry picked from commit 31a3f2bdee)
2022-04-25 10:45:35 -05:00
github-actions[bot]
106da05db7 Bumping version to 1.1.0rc2 (#5129)
* Bumping version to 1.1.0rc2

* release changelog

* fix trailing space

Co-authored-by: Github Build Bot <buildbot@fishtownanalytics.com>
Co-authored-by: Chenyu Li <chenyu.li@dbtlabs.com>
2022-04-21 09:52:08 -06:00
Chenyu Li
09a396a731 Restore ability to utilize updated_at for check_cols snapshots (#5077) (#5126)
* Restore ability to configure and utilize `updated_at` for snapshots using the check_cols strategy

* Changelog entry

* Optional comparison of column names starting with `dbt_`

* Functional test for check cols snapshots using `updated_at`

* Comments to explain the test implementation

(cherry picked from commit d09459c980)

Co-authored-by: Doug Beatty <44704949+dbeatty10@users.noreply.github.com>
2022-04-21 09:06:04 -06:00
Emily Rockman
9c233f27ab remove commas from changelog yaml (#5113) 2022-04-19 19:57:14 -05:00
github-actions[bot]
a09bc28768 Bumping version to 1.1.0rc1 (#5044)
* Bumping version to 1.1.0rc1

* Adding changelog

* Updating adapter versions for Docker

* Remove extra spaces

Co-authored-by: Github Build Bot <buildbot@fishtownanalytics.com>
Co-authored-by: Leah Antkiewicz <leah.antkiewicz@fishtownanalytics.com>
Co-authored-by: leahwicz <60146280+leahwicz@users.noreply.github.com>
2022-04-12 17:10:57 -04:00
1146 changed files with 47739 additions and 59713 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 1.5.0a1 current_version = 1.1.0
parse = (?P<major>\d+) parse = (?P<major>\d+)
\.(?P<minor>\d+) \.(?P<minor>\d+)
\.(?P<patch>\d+) \.(?P<patch>\d+)
@@ -28,6 +28,8 @@ first_value = 1
[bumpversion:file:core/dbt/version.py] [bumpversion:file:core/dbt/version.py]
[bumpversion:file:core/scripts/create_adapter_plugins.py]
[bumpversion:file:plugins/postgres/setup.py] [bumpversion:file:plugins/postgres/setup.py]
[bumpversion:file:plugins/postgres/dbt/adapters/postgres/__version__.py] [bumpversion:file:plugins/postgres/dbt/adapters/postgres/__version__.py]

View File

@@ -2,11 +2,6 @@
For information on prior major and minor releases, see their changelogs: For information on prior major and minor releases, see their changelogs:
* [1.4](https://github.com/dbt-labs/dbt-core/blob/1.4.latest/CHANGELOG.md)
* [1.3](https://github.com/dbt-labs/dbt-core/blob/1.3.latest/CHANGELOG.md)
* [1.2](https://github.com/dbt-labs/dbt-core/blob/1.2.latest/CHANGELOG.md)
* [1.1](https://github.com/dbt-labs/dbt-core/blob/1.1.latest/CHANGELOG.md)
* [1.0](https://github.com/dbt-labs/dbt-core/blob/1.0.latest/CHANGELOG.md) * [1.0](https://github.com/dbt-labs/dbt-core/blob/1.0.latest/CHANGELOG.md)
* [0.21](https://github.com/dbt-labs/dbt-core/blob/0.21.latest/CHANGELOG.md) * [0.21](https://github.com/dbt-labs/dbt-core/blob/0.21.latest/CHANGELOG.md)
* [0.20](https://github.com/dbt-labs/dbt-core/blob/0.20.latest/CHANGELOG.md) * [0.20](https://github.com/dbt-labs/dbt-core/blob/0.20.latest/CHANGELOG.md)

96
.changes/1.1.0.md Normal file
View File

@@ -0,0 +1,96 @@
## dbt-core 1.1.0 - April 28, 2022
### Breaking Changes
- **Relevant to maintainers of adapter plugins _only_:** The abstractmethods `get_response` and `execute` now only return a `connection.AdapterReponse` in type hints. (Previously, they could return a string.) We encourage you to update your methods to return an object of class `AdapterResponse`, or implement a subclass specific to your adapter
([#4499](https://github.com/dbt-labs/dbt-core/issues/4499), [#4869](https://github.com/dbt-labs/dbt-core/pull/4869))
- For adapter plugin maintainers only: Internal adapter methods `set_relations_cache` + `_relations_cache_for_schemas` each take an additional argument, for use with experimental `CACHE_SELECTED_ONLY` config ([#4688](https://github.com/dbt-labs/dbt-core/issues/4688), [#4860](https://github.com/dbt-labs/dbt-core/pull/4860))
### Features
- Change behaviour of `non_null` test so that it only `select`s all columns if `--store-failures` is enabled. ([#4769](https://github.com/dbt-labs/dbt-core/issues/4769), [#4777](https://github.com/dbt-labs/dbt-core/pull/4777))
- Testing framework for dbt adapter testing ([#4730](https://github.com/dbt-labs/dbt-core/issues/4730), [#4846](https://github.com/dbt-labs/dbt-core/pull/4846))
- Allow unique key to take a list implementation for postgres/redshift ([#4738](https://github.com/dbt-labs/dbt-core/issues/4738), [#4858](https://github.com/dbt-labs/dbt-core/pull/4858))
- Add `--cache_selected_only` flag to cache schema object of selected models only. ([#4688](https://github.com/dbt-labs/dbt-core/issues/4688), [#4860](https://github.com/dbt-labs/dbt-core/pull/4860))
- Support custom names for generic tests ([#3348](https://github.com/dbt-labs/dbt-core/issues/3348), [#4898](https://github.com/dbt-labs/dbt-core/pull/4898))
- Added Support for Semantic Versioning ([#4453](https://github.com/dbt-labs/dbt-core/issues/4453), [#4644](https://github.com/dbt-labs/dbt-core/pull/4644))
- New Dockerfile to support specific db adapters and platforms. See docker/README.md for details ([#4495](https://github.com/dbt-labs/dbt-core/issues/4495), [#4487](https://github.com/dbt-labs/dbt-core/pull/4487))
- Allow unique_key to take a list ([#2479](https://github.com/dbt-labs/dbt-core/issues/2479), [#4618](https://github.com/dbt-labs/dbt-core/pull/4618))
- Add `--quiet` global flag and `print` Jinja function ([#3451](https://github.com/dbt-labs/dbt-core/issues/3451), [#4701](https://github.com/dbt-labs/dbt-core/pull/4701))
- Add space before justification periods ([#4737](https://github.com/dbt-labs/dbt-core/issues/4737), [#4744](https://github.com/dbt-labs/dbt-core/pull/4744))
- Enable dbt jobs to run downstream models based on fresher sources. Compare the source freshness results between previous and current state. If any source is fresher and/or new in current vs. previous state, dbt will run and test the downstream models in scope. Example command: `dbt build --select source_status:fresher+` ([#4050](https://github.com/dbt-labs/dbt-core/issues/4050), [#4256](https://github.com/dbt-labs/dbt-core/pull/4256))
- converting unique key as list tests to new pytest format ([#4882](https://github.com/dbt-labs/dbt-core/issues/4882), [#4958](https://github.com/dbt-labs/dbt-core/pull/4958))
- Add a variable called selected_resources in the Jinja context containing a list of all the resources matching the nodes for the --select, --exclude and/or --selector parameters. ([#3471](https://github.com/dbt-labs/dbt-core/issues/3471), [#5001](https://github.com/dbt-labs/dbt-core/pull/5001))
- Support the DO_NOT_TRACK environment variable from the consoledonottrack.com initiative ([#3540](https://github.com/dbt-labs/dbt-core/issues/3540), [#5000](https://github.com/dbt-labs/dbt-core/pull/5000))
- Add `--no-print` global flag ([#4710](https://github.com/dbt-labs/dbt-core/issues/4710), [#4854](https://github.com/dbt-labs/dbt-core/pull/4854))
- add enabled as a source config ([#3662](https://github.com/dbt-labs/dbt-core/issues/3662), [#5008](https://github.com/dbt-labs/dbt-core/pull/5008))
### Fixes
- Fix bug causing empty node level meta, snapshot config errors ([#4459](https://github.com/dbt-labs/dbt-core/issues/4459), [#4726](https://github.com/dbt-labs/dbt-core/pull/4726))
- Inconsistent timestamps between inserted/updated and deleted rows in snapshots ([#4347](https://github.com/dbt-labs/dbt-core/issues/4347), [#4513](https://github.com/dbt-labs/dbt-core/pull/4513))
- Catch all Requests Exceptions on deps install to attempt retries. Also log the exceptions hit. ([#4849](https://github.com/dbt-labs/dbt-core/issues/4849), [#4865](https://github.com/dbt-labs/dbt-core/pull/4865))
- Use cli_vars instead of context to create package and selector renderers ([#4876](https://github.com/dbt-labs/dbt-core/issues/4876), [#4878](https://github.com/dbt-labs/dbt-core/pull/4878))
- depend on new dbt-extractor version with fixed github links ([#4891](https://github.com/dbt-labs/dbt-core/issues/4891), [#4890](https://github.com/dbt-labs/dbt-core/pull/4890))
- Update bumpervsion config to stop looking for missing setup.py ([#-1](https://github.com/dbt-labs/dbt-core/issues/-1), [#4896](https://github.com/dbt-labs/dbt-core/pull/4896))
- Corrected permissions settings for docker release workflow ([#4902](https://github.com/dbt-labs/dbt-core/issues/4902), [#4903](https://github.com/dbt-labs/dbt-core/pull/4903))
- User wasn't asked for permission to overwite a profile entry when running init inside an existing project ([#4375](https://github.com/dbt-labs/dbt-core/issues/4375), [#4447](https://github.com/dbt-labs/dbt-core/pull/4447))
- Add project name validation to `dbt init` ([#4490](https://github.com/dbt-labs/dbt-core/issues/4490), [#4536](https://github.com/dbt-labs/dbt-core/pull/4536))
- Allow override of string and numeric types for adapters. ([#4603](https://github.com/dbt-labs/dbt-core/issues/4603), [#4604](https://github.com/dbt-labs/dbt-core/pull/4604))
- A change in secret environment variables won't trigger a full reparse ([#4650](https://github.com/dbt-labs/dbt-core/issues/4650), [#4665](https://github.com/dbt-labs/dbt-core/pull/4665))
- Fix misspellings and typos in docstrings ([#4904](https://github.com/dbt-labs/dbt-core/issues/4904), [#4545](https://github.com/dbt-labs/dbt-core/pull/4545))
- Catch more cases to retry package retrieval for deps pointing to the hub. Also start to cache the package requests. ([#4849](https://github.com/dbt-labs/dbt-core/issues/4849), [#4982](https://github.com/dbt-labs/dbt-core/pull/4982))
- Make the warning message for a full event deque more descriptive ([#4962](https://github.com/dbt-labs/dbt-core/issues/4962), [#5011](https://github.com/dbt-labs/dbt-core/pull/5011))
- Fix hard delete snapshot test ([#4916](https://github.com/dbt-labs/dbt-core/issues/4916), [#5020](https://github.com/dbt-labs/dbt-core/pull/5020))
- Restore ability to utilize `updated_at` for check_cols snapshots ([#5076](https://github.com/dbt-labs/dbt-core/issues/5076), [#5077](https://github.com/dbt-labs/dbt-core/pull/5077))
- Use yaml renderer (with target context) for rendering selectors ([#5131](https://github.com/dbt-labs/dbt-core/issues/5131), [#5136](https://github.com/dbt-labs/dbt-core/pull/5136))
- Fix retry logic to return values after initial try ([#5023](https://github.com/dbt-labs/dbt-core/issues/5023), [#5137](https://github.com/dbt-labs/dbt-core/pull/5137))
- Scrub secret env vars from CommandError in exception stacktrace ([#5151](https://github.com/dbt-labs/dbt-core/issues/5151), [#5152](https://github.com/dbt-labs/dbt-core/pull/5152))
### Docs
- Resolve errors related to operations preventing DAG from generating in the docs. Also patch a spark issue to allow search to filter accurately past the missing columns. ([#4578](https://github.com/dbt-labs/dbt-core/issues/4578), [#4763](https://github.com/dbt-labs/dbt-core/pull/4763))
- Fixed capitalization in UI for exposures of `type: ml` ([#4984](https://github.com/dbt-labs/dbt-core/issues/4984), [#4995](https://github.com/dbt-labs/dbt-core/pull/4995))
- List packages and tags in alphabetical order ([#4984](https://github.com/dbt-labs/dbt-core/issues/4984), [#4995](https://github.com/dbt-labs/dbt-core/pull/4995))
- Bump jekyll from 3.8.7 to 3.9.0 ([#4984](https://github.com/dbt-labs/dbt-core/issues/4984), [#4995](https://github.com/dbt-labs/dbt-core/pull/4995))
- Updated docker README to reflect necessity of using BuildKit ([#4990](https://github.com/dbt-labs/dbt-core/issues/4990), [#5018](https://github.com/dbt-labs/dbt-core/pull/5018))
### Under the Hood
- Automate changelog generation with changie ([#4652](https://github.com/dbt-labs/dbt-core/issues/4652), [#4743](https://github.com/dbt-labs/dbt-core/pull/4743))
- add performance regression testing runner without orchestration ([#4021](https://github.com/dbt-labs/dbt-core/issues/4021), [#4602](https://github.com/dbt-labs/dbt-core/pull/4602))
- Fix broken links for changelog generation and tweak GHA to only post a comment once when changelog entry is missing. ([#4848](https://github.com/dbt-labs/dbt-core/issues/4848), [#4857](https://github.com/dbt-labs/dbt-core/pull/4857))
- Add support for Python 3.10 ([#4562](https://github.com/dbt-labs/dbt-core/issues/4562), [#4866](https://github.com/dbt-labs/dbt-core/pull/4866))
- Enable more dialects to snapshot sources with added columns, even those that don't support boolean datatypes ([#4488](https://github.com/dbt-labs/dbt-core/issues/4488), [#4871](https://github.com/dbt-labs/dbt-core/pull/4871))
- Add Graph Compilation and Adapter Cache tracking ([#4625](https://github.com/dbt-labs/dbt-core/issues/4625), [#4912](https://github.com/dbt-labs/dbt-core/pull/4912))
- Testing cleanup ([#3648](https://github.com/dbt-labs/dbt-core/issues/3648), [#4509](https://github.com/dbt-labs/dbt-core/pull/4509))
- Clean up test deprecation warnings ([#3988](https://github.com/dbt-labs/dbt-core/issues/3988), [#4556](https://github.com/dbt-labs/dbt-core/pull/4556))
- Use mashumaro for serialization in event logging ([#4504](https://github.com/dbt-labs/dbt-core/issues/4504), [#4505](https://github.com/dbt-labs/dbt-core/pull/4505))
- Drop support for Python 3.7.0 + 3.7.1 ([#4584](https://github.com/dbt-labs/dbt-core/issues/4584), [#4585](https://github.com/dbt-labs/dbt-core/pull/4585))
- Drop support for Python <3.7.2 ([#4584](https://github.com/dbt-labs/dbt-core/issues/4584), [#4643](https://github.com/dbt-labs/dbt-core/pull/4643))
- Re-format codebase (except tests) using pre-commit hooks ([#3195](https://github.com/dbt-labs/dbt-core/issues/3195), [#4697](https://github.com/dbt-labs/dbt-core/pull/4697))
- Add deps module README ([#4904](https://github.com/dbt-labs/dbt-core/issues/4904), [#4686](https://github.com/dbt-labs/dbt-core/pull/4686))
- Initial conversion of tests to pytest ([#4690](https://github.com/dbt-labs/dbt-core/issues/4690), [#4691](https://github.com/dbt-labs/dbt-core/pull/4691))
- Fix errors in Windows for tests/functions ([#4782](https://github.com/dbt-labs/dbt-core/issues/4782), [#4767](https://github.com/dbt-labs/dbt-core/pull/4767))
- Create a dbt.tests.adapter release when releasing dbt and postgres ([#4812](https://github.com/dbt-labs/dbt-core/issues/4812), [#4948](https://github.com/dbt-labs/dbt-core/pull/4948))
- update docker image to use python 3.10.3 ([#4904](https://github.com/dbt-labs/dbt-core/issues/4904), [#4963](https://github.com/dbt-labs/dbt-core/pull/4963))
- updates black to 22.3.0 which fixes dependency incompatibility when running with precommit. ([#4904](https://github.com/dbt-labs/dbt-core/issues/4904), [#4972](https://github.com/dbt-labs/dbt-core/pull/4972))
- Adds config util for ad-hoc creation of project objs or dicts ([#4808](https://github.com/dbt-labs/dbt-core/issues/4808), [#4981](https://github.com/dbt-labs/dbt-core/pull/4981))
- Remove TableComparison and convert existing calls to use dbt.tests.util ([#4778](https://github.com/dbt-labs/dbt-core/issues/4778), [#4986](https://github.com/dbt-labs/dbt-core/pull/4986))
- Remove unneeded create_schema in snapshot materialization ([#4742](https://github.com/dbt-labs/dbt-core/issues/4742), [#4993](https://github.com/dbt-labs/dbt-core/pull/4993))
- Added .git-blame-ignore-revs file to mask re-formmating commits from git blame ([#5004](https://github.com/dbt-labs/dbt-core/issues/5004), [#5019](https://github.com/dbt-labs/dbt-core/pull/5019))
- Convert version tests to pytest ([#5024](https://github.com/dbt-labs/dbt-core/issues/5024), [#5026](https://github.com/dbt-labs/dbt-core/pull/5026))
- Updating tests and docs to show that we now support Python 3.10 ([#4974](https://github.com/dbt-labs/dbt-core/issues/4974), [#5025](https://github.com/dbt-labs/dbt-core/pull/5025))
- Update --version output and logic ([#4724](https://github.com/dbt-labs/dbt-core/issues/4724), [#5029](https://github.com/dbt-labs/dbt-core/pull/5029))
- ([#5033](https://github.com/dbt-labs/dbt-core/issues/5033), [#5032](https://github.com/dbt-labs/dbt-core/pull/5032))
### Contributors
- [@NiallRees](https://github.com/NiallRees) ([#4447](https://github.com/dbt-labs/dbt-core/pull/4447))
- [@agoblet](https://github.com/agoblet) ([#5000](https://github.com/dbt-labs/dbt-core/pull/5000))
- [@alswang18](https://github.com/alswang18) ([#4644](https://github.com/dbt-labs/dbt-core/pull/4644))
- [@amirkdv](https://github.com/amirkdv) ([#4536](https://github.com/dbt-labs/dbt-core/pull/4536))
- [@anaisvaillant](https://github.com/anaisvaillant) ([#4256](https://github.com/dbt-labs/dbt-core/pull/4256))
- [@b-per](https://github.com/b-per) ([#5001](https://github.com/dbt-labs/dbt-core/pull/5001))
- [@dbeatty10](https://github.com/dbeatty10) ([#5077](https://github.com/dbt-labs/dbt-core/pull/5077))
- [@ehmartens](https://github.com/ehmartens) ([#4701](https://github.com/dbt-labs/dbt-core/pull/4701))
- [@joellabes](https://github.com/joellabes) ([#4744](https://github.com/dbt-labs/dbt-core/pull/4744))
- [@jonstacks](https://github.com/jonstacks) ([#4995](https://github.com/dbt-labs/dbt-core/pull/4995))
- [@kadero](https://github.com/kadero) ([#4513](https://github.com/dbt-labs/dbt-core/pull/4513))
- [@karunpoudel](https://github.com/karunpoudel) ([#4860](https://github.com/dbt-labs/dbt-core/pull/4860), [#4860](https://github.com/dbt-labs/dbt-core/pull/4860))
- [@kazanzhy](https://github.com/kazanzhy) ([#4545](https://github.com/dbt-labs/dbt-core/pull/4545))
- [@matt-winkler](https://github.com/matt-winkler) ([#4256](https://github.com/dbt-labs/dbt-core/pull/4256))
- [@mdesmet](https://github.com/mdesmet) ([#4604](https://github.com/dbt-labs/dbt-core/pull/4604))
- [@pgoslatara](https://github.com/pgoslatara) ([#4995](https://github.com/dbt-labs/dbt-core/pull/4995))
- [@poloaraujo](https://github.com/poloaraujo) ([#4854](https://github.com/dbt-labs/dbt-core/pull/4854))
- [@sungchun12](https://github.com/sungchun12) ([#4256](https://github.com/dbt-labs/dbt-core/pull/4256))
- [@willbowditch](https://github.com/willbowditch) ([#4777](https://github.com/dbt-labs/dbt-core/pull/4777))

View File

@@ -1,6 +0,0 @@
kind: Features
body: Adding the entity node
time: 2023-01-18T13:48:04.487817-06:00
custom:
Author: callum-mcdata
Issue: "6627"

View File

@@ -1,6 +0,0 @@
kind: Under the Hood
body: Fix use of ConnectionReused logging event
time: 2023-01-13T13:25:13.023168-05:00
custom:
Author: gshank
Issue: "6168"

View File

@@ -1,6 +0,0 @@
kind: Under the Hood
body: Update deprecated github action command
time: 2023-01-17T11:17:37.046095-06:00
custom:
Author: davidbloss
Issue: "6153"

103
.changie.yaml Normal file → Executable file
View File

@@ -6,114 +6,47 @@ changelogPath: CHANGELOG.md
versionExt: md versionExt: md
versionFormat: '## dbt-core {{.Version}} - {{.Time.Format "January 02, 2006"}}' versionFormat: '## dbt-core {{.Version}} - {{.Time.Format "January 02, 2006"}}'
kindFormat: '### {{.Kind}}' kindFormat: '### {{.Kind}}'
changeFormat: |- changeFormat: '- {{.Body}} ([#{{.Custom.Issue}}](https://github.com/dbt-labs/dbt-core/issues/{{.Custom.Issue}}), [#{{.Custom.PR}}](https://github.com/dbt-labs/dbt-core/pull/{{.Custom.PR}}))'
{{- $IssueList := list }}
{{- $changes := splitList " " $.Custom.Issue }}
{{- range $issueNbr := $changes }}
{{- $changeLink := "[#nbr](https://github.com/dbt-labs/dbt-core/issues/nbr)" | replace "nbr" $issueNbr }}
{{- $IssueList = append $IssueList $changeLink }}
{{- end -}}
- {{.Body}} ({{ range $index, $element := $IssueList }}{{if $index}}, {{end}}{{$element}}{{end}})
kinds: kinds:
- label: Breaking Changes - label: Breaking Changes
- label: Features - label: Features
- label: Fixes - label: Fixes
- label: Docs - label: Docs
changeFormat: |-
{{- $IssueList := list }}
{{- $changes := splitList " " $.Custom.Issue }}
{{- range $issueNbr := $changes }}
{{- $changeLink := "[dbt-docs/#nbr](https://github.com/dbt-labs/dbt-docs/issues/nbr)" | replace "nbr" $issueNbr }}
{{- $IssueList = append $IssueList $changeLink }}
{{- end -}}
- {{.Body}} ({{ range $index, $element := $IssueList }}{{if $index}}, {{end}}{{$element}}{{end}})
- label: Under the Hood - label: Under the Hood
- label: Dependencies - label: Dependencies
changeFormat: |-
{{- $PRList := list }}
{{- $changes := splitList " " $.Custom.PR }}
{{- range $pullrequest := $changes }}
{{- $changeLink := "[#nbr](https://github.com/dbt-labs/dbt-core/pull/nbr)" | replace "nbr" $pullrequest }}
{{- $PRList = append $PRList $changeLink }}
{{- end -}}
- {{.Body}} ({{ range $index, $element := $PRList }}{{if $index}}, {{end}}{{$element}}{{end}})
skipGlobalChoices: true
additionalChoices:
- key: Author
label: GitHub Username(s) (separated by a single space if multiple)
type: string
minLength: 3
- key: PR
label: GitHub Pull Request Number (separated by a single space if multiple)
type: string
minLength: 1
- label: Security
changeFormat: |-
{{- $PRList := list }}
{{- $changes := splitList " " $.Custom.PR }}
{{- range $pullrequest := $changes }}
{{- $changeLink := "[#nbr](https://github.com/dbt-labs/dbt-core/pull/nbr)" | replace "nbr" $pullrequest }}
{{- $PRList = append $PRList $changeLink }}
{{- end -}}
- {{.Body}} ({{ range $index, $element := $PRList }}{{if $index}}, {{end}}{{$element}}{{end}})
skipGlobalChoices: true
additionalChoices:
- key: Author
label: GitHub Username(s) (separated by a single space if multiple)
type: string
minLength: 3
- key: PR
label: GitHub Pull Request Number (separated by a single space if multiple)
type: string
minLength: 1
newlines:
afterChangelogHeader: 1
afterKind: 1
afterChangelogVersion: 1
beforeKind: 1
endOfVersion: 1
custom: custom:
- key: Author - key: Author
label: GitHub Username(s) (separated by a single space if multiple) label: GitHub Username(s) (separated by a single space if multiple)
type: string type: string
minLength: 3 minLength: 3
- key: Issue - key: Issue
label: GitHub Issue Number (separated by a single space if multiple) label: GitHub Issue Number
type: string type: int
minLength: 1 minLength: 4
- key: PR
label: GitHub Pull Request Number
type: int
minLength: 4
footerFormat: | footerFormat: |
{{- $contributorDict := dict }} {{- $contributorDict := dict }}
{{- /* any names added to this list should be all lowercase for later matching purposes */}} {{- /* any names added to this list should be all lowercase for later matching purposes */}}
{{- $core_team := list "michelleark" "peterallenwebb" "emmyoop" "nathaniel-may" "gshank" "leahwicz" "chenyulinx" "stu-k" "iknox-fa" "versusfacit" "mcknight-42" "jtcohen6" "aranke" "dependabot[bot]" "snyk-bot" "colin-rogers-dbt" }} {{- $core_team := list "emmyoop" "nathaniel-may" "gshank" "leahwicz" "chenyulinx" "stu-k" "iknox-fa" "versusfacit" "mcknight-42" "jtcohen6" "dependabot" }}
{{- range $change := .Changes }} {{- range $change := .Changes }}
{{- $authorList := splitList " " $change.Custom.Author }} {{- $authorList := splitList " " $change.Custom.Author }}
{{- /* loop through all authors for a single changelog */}} {{- /* loop through all authors for a PR */}}
{{- range $author := $authorList }} {{- range $author := $authorList }}
{{- $authorLower := lower $author }} {{- $authorLower := lower $author }}
{{- /* we only want to include non-core team contributors */}} {{- /* we only want to include non-core team contributors */}}
{{- if not (has $authorLower $core_team)}} {{- if not (has $authorLower $core_team)}}
{{- $changeList := splitList " " $change.Custom.Author }} {{- $pr := $change.Custom.PR }}
{{- /* Docs kind link back to dbt-docs instead of dbt-core issues */}} {{- /* check if this contributor has other PRs associated with them already */}}
{{- $changeLink := $change.Kind }}
{{- if or (eq $change.Kind "Dependencies") (eq $change.Kind "Security") }}
{{- $changeLink = "[#nbr](https://github.com/dbt-labs/dbt-core/pull/nbr)" | replace "nbr" $change.Custom.PR }}
{{- else if eq $change.Kind "Docs"}}
{{- $changeLink = "[dbt-docs/#nbr](https://github.com/dbt-labs/dbt-docs/issues/nbr)" | replace "nbr" $change.Custom.Issue }}
{{- else }}
{{- $changeLink = "[#nbr](https://github.com/dbt-labs/dbt-core/issues/nbr)" | replace "nbr" $change.Custom.Issue }}
{{- end }}
{{- /* check if this contributor has other changes associated with them already */}}
{{- if hasKey $contributorDict $author }} {{- if hasKey $contributorDict $author }}
{{- $contributionList := get $contributorDict $author }} {{- $prList := get $contributorDict $author }}
{{- $contributionList = append $contributionList $changeLink }} {{- $prList = append $prList $pr }}
{{- $contributorDict := set $contributorDict $author $contributionList }} {{- $contributorDict := set $contributorDict $author $prList }}
{{- else }} {{- else }}
{{- $contributionList := list $changeLink }} {{- $prList := list $change.Custom.PR }}
{{- $contributorDict := set $contributorDict $author $contributionList }} {{- $contributorDict := set $contributorDict $author $prList }}
{{- end }} {{- end }}
{{- end}} {{- end}}
{{- end}} {{- end}}
@@ -122,6 +55,6 @@ footerFormat: |
{{- if $contributorDict}} {{- if $contributorDict}}
### Contributors ### Contributors
{{- range $k,$v := $contributorDict }} {{- range $k,$v := $contributorDict }}
- [@{{$k}}](https://github.com/{{$k}}) ({{ range $index, $element := $v }}{{if $index}}, {{end}}{{$element}}{{end}}) - [@{{$k}}](https://github.com/{{$k}}) ({{ range $index, $element := $v }}{{if $index}}, {{end}}[#{{$element}}](https://github.com/dbt-labs/dbt-core/pull/{{$element}}){{end}})
{{- end }} {{- end }}
{{- end }} {{- end }}

View File

@@ -9,4 +9,4 @@ ignore =
E203 # makes Flake8 work like black E203 # makes Flake8 work like black
E741 E741
E501 # long line checking is done in black E501 # long line checking is done in black
exclude = test/ exclude = test

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
core/dbt/include/index.html binary
tests/functional/artifacts/data/state/*/manifest.json binary

42
.github/CODEOWNERS vendored
View File

@@ -16,57 +16,25 @@
# Changes to GitHub configurations including Actions # Changes to GitHub configurations including Actions
/.github/ @leahwicz /.github/ @leahwicz
### LANGUAGE
# Language core modules # Language core modules
/core/dbt/config/ @dbt-labs/core-language /core/dbt/config/ @dbt-labs/core-language
/core/dbt/context/ @dbt-labs/core-language /core/dbt/context/ @dbt-labs/core-language
/core/dbt/contracts/ @dbt-labs/core-language /core/dbt/contracts/ @dbt-labs/core-language
/core/dbt/deps/ @dbt-labs/core-language /core/dbt/deps/ @dbt-labs/core-language
/core/dbt/events/ @dbt-labs/core-language # structured logging
/core/dbt/parser/ @dbt-labs/core-language /core/dbt/parser/ @dbt-labs/core-language
# Language misc files
/core/dbt/dataclass_schema.py @dbt-labs/core-language
/core/dbt/hooks.py @dbt-labs/core-language
/core/dbt/node_types.py @dbt-labs/core-language
/core/dbt/semver.py @dbt-labs/core-language
### EXECUTION
# Execution core modules # Execution core modules
/core/dbt/events/ @dbt-labs/core-execution @dbt-labs/core-language # eventually remove language but they have knowledge here now
/core/dbt/graph/ @dbt-labs/core-execution /core/dbt/graph/ @dbt-labs/core-execution
/core/dbt/task/ @dbt-labs/core-execution /core/dbt/task/ @dbt-labs/core-execution
# Execution misc files # Adapter interface, scaffold, Postgres plugin
/core/dbt/compilation.py @dbt-labs/core-execution
/core/dbt/flags.py @dbt-labs/core-execution
/core/dbt/lib.py @dbt-labs/core-execution
/core/dbt/main.py @dbt-labs/core-execution
/core/dbt/profiler.py @dbt-labs/core-execution
/core/dbt/selected_resources.py @dbt-labs/core-execution
/core/dbt/tracking.py @dbt-labs/core-execution
/core/dbt/version.py @dbt-labs/core-execution
### ADAPTERS
# Adapter interface ("base" + "sql" adapter defaults, cache)
/core/dbt/adapters @dbt-labs/core-adapters /core/dbt/adapters @dbt-labs/core-adapters
/core/scripts/create_adapter_plugin.py @dbt-labs/core-adapters
# Global project (default macros + materializations), starter project
/core/dbt/include @dbt-labs/core-adapters
# Postgres plugin
/plugins/ @dbt-labs/core-adapters /plugins/ @dbt-labs/core-adapters
# Functional tests for adapter plugins # Global project: default macros, including generic tests + materializations
/tests/adapter @dbt-labs/core-adapters /core/dbt/include/global_project @dbt-labs/core-execution @dbt-labs/core-adapters
### TESTS
# Overlapping ownership for vast majority of unit + functional tests
# Perf regression testing framework # Perf regression testing framework
# This excludes the test project files itself since those aren't specific # This excludes the test project files itself since those aren't specific

View File

@@ -9,33 +9,23 @@ body:
Thanks for taking the time to fill out this bug report! Thanks for taking the time to fill out this bug report!
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is this a new bug in dbt-core? label: Is there an existing issue for this?
description: > description: Please search to see if an issue already exists for the bug you encountered.
In other words, is this an error, flaw, failure or fault in our software?
If this is a bug that broke existing functionality that used to work, please open a regression issue.
If this is a bug in an adapter plugin, please open an issue in the adapter's repository.
If this is a bug experienced while using dbt Cloud, please report to [support](mailto:support@getdbt.com).
If this is a request for help or troubleshooting code in your own dbt project, please join our [dbt Community Slack](https://www.getdbt.com/community/join-the-community/) or open a [Discussion question](https://github.com/dbt-labs/docs.getdbt.com/discussions).
Please search to see if an issue already exists for the bug you encountered.
options: options:
- label: I believe this is a new bug in dbt-core - label: I have searched the existing issues
required: true
- label: I have searched the existing issues, and I could not find an existing issue for this bug
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Current Behavior label: Current Behavior
description: A concise description of what you're experiencing. description: A concise description of what you're experiencing.
validations: validations:
required: true required: false
- type: textarea - type: textarea
attributes: attributes:
label: Expected Behavior label: Expected Behavior
description: A concise description of what you expected to happen. description: A concise description of what you expected to happen.
validations: validations:
required: true required: false
- type: textarea - type: textarea
attributes: attributes:
label: Steps To Reproduce label: Steps To Reproduce
@@ -46,7 +36,7 @@ body:
3. Run '...' 3. Run '...'
4. See error... 4. See error...
validations: validations:
required: true required: false
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:
@@ -62,8 +52,8 @@ body:
description: | description: |
examples: examples:
- **OS**: Ubuntu 20.04 - **OS**: Ubuntu 20.04
- **Python**: 3.9.12 (`python3 --version`) - **Python**: 3.7.2 (`python --version`)
- **dbt-core**: 1.1.1 (`dbt --version`) - **dbt**: 0.21.0 (`dbt --version`)
value: | value: |
- OS: - OS:
- Python: - Python:
@@ -74,15 +64,13 @@ body:
- type: dropdown - type: dropdown
id: database id: database
attributes: attributes:
label: Which database adapter are you using with dbt? label: What database are you using dbt with?
description: If the bug is specific to the database or adapter, please open the issue in that adapter's repository instead
multiple: true multiple: true
options: options:
- postgres - postgres
- redshift - redshift
- snowflake - snowflake
- bigquery - bigquery
- spark
- other (mention it in "Additional Context") - other (mention it in "Additional Context")
validations: validations:
required: false required: false

View File

@@ -1,14 +1,4 @@
blank_issues_enabled: false
contact_links: contact_links:
- name: Ask the community for help
url: https://github.com/dbt-labs/docs.getdbt.com/discussions
about: Need help troubleshooting? Check out our guide on how to ask
- name: Contact dbt Cloud support
url: mailto:support@getdbt.com
about: Are you using dbt Cloud? Contact our support team for help!
- name: Participate in Discussions
url: https://github.com/dbt-labs/dbt-core/discussions
about: Do you have a Big Idea for dbt? Read open discussions, or start a new one
- name: Create an issue for dbt-redshift - name: Create an issue for dbt-redshift
url: https://github.com/dbt-labs/dbt-redshift/issues/new/choose url: https://github.com/dbt-labs/dbt-redshift/issues/new/choose
about: Report a bug or request a feature for dbt-redshift about: Report a bug or request a feature for dbt-redshift
@@ -18,6 +8,9 @@ contact_links:
- name: Create an issue for dbt-snowflake - name: Create an issue for dbt-snowflake
url: https://github.com/dbt-labs/dbt-snowflake/issues/new/choose url: https://github.com/dbt-labs/dbt-snowflake/issues/new/choose
about: Report a bug or request a feature for dbt-snowflake about: Report a bug or request a feature for dbt-snowflake
- name: Create an issue for dbt-spark - name: Ask a question or get support
url: https://github.com/dbt-labs/dbt-spark/issues/new/choose url: https://docs.getdbt.com/docs/guides/getting-help
about: Report a bug or request a feature for dbt-spark about: Ask a question or request support
- name: Questions on Stack Overflow
url: https://stackoverflow.com/questions/tagged/dbt
about: Look at questions/answers at Stack Overflow

View File

@@ -1,32 +1,22 @@
name: ✨ Feature name: ✨ Feature
description: Propose a straightforward extension of dbt functionality description: Suggest an idea for dbt
title: "[Feature] <title>" title: "[Feature] <title>"
labels: ["enhancement", "triage"] labels: ["enhancement", "triage"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this feature request! Thanks for taking the time to fill out this feature requests!
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is this your first time submitting a feature request? label: Is there an existing feature request for this?
description: > description: Please search to see if an issue already exists for the feature you would like.
We want to make sure that features are distinct and discoverable,
so that other members of the community can find them and offer their thoughts.
Issues are the right place to request straightforward extensions of existing dbt functionality.
For "big ideas" about future capabilities of dbt, we ask that you open a
[discussion](https://github.com/dbt-labs/dbt-core/discussions) in the "Ideas" category instead.
options: options:
- label: I have read the [expectations for open source contributors](https://docs.getdbt.com/docs/contributing/oss-expectations) - label: I have searched the existing issues
required: true
- label: I have searched the existing issues, and I could not find an existing issue for this feature
required: true
- label: I am requesting a straightforward extension of existing dbt functionality, rather than a Big Idea better suited to a discussion
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Describe the feature label: Describe the Feature
description: A clear and concise description of what you want to happen. description: A clear and concise description of what you want to happen.
validations: validations:
required: true required: true

View File

@@ -1,93 +0,0 @@
name: ☣️ Regression
description: Report a regression you've observed in a newer version of dbt
title: "[Regression] <title>"
labels: ["bug", "regression", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this regression report!
- type: checkboxes
attributes:
label: Is this a regression in a recent version of dbt-core?
description: >
A regression is when documented functionality works as expected in an older version of dbt-core,
and no longer works after upgrading to a newer version of dbt-core
options:
- label: I believe this is a regression in dbt-core functionality
required: true
- label: I have searched the existing issues, and I could not find an existing issue for this regression
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected/Previous Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: |
If applicable, log output to help explain your problem.
render: shell
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **OS**: Ubuntu 20.04
- **Python**: 3.9.12 (`python3 --version`)
- **dbt-core (working version)**: 1.1.1 (`dbt --version`)
- **dbt-core (regression version)**: 1.2.0 (`dbt --version`)
value: |
- OS:
- Python:
- dbt (working version):
- dbt (regression version):
render: markdown
validations:
required: true
- type: dropdown
id: database
attributes:
label: Which database adapter are you using with dbt?
description: If the regression is specific to the database or adapter, please open the issue in that adapter's repository instead
multiple: true
options:
- postgres
- redshift
- snowflake
- bigquery
- spark
- other (mention it in "Additional Context")
validations:
required: false
- type: textarea
attributes:
label: Additional Context
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false

216
.github/_README.md vendored
View File

@@ -1,216 +0,0 @@
<!-- GitHub will publish this readme on the main repo page if the name is `README.md` so we've added the leading underscore to prevent this -->
<!-- Do not rename this file `README.md` -->
<!-- See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-readmes -->
## What are GitHub Actions?
GitHub Actions are used for many different purposes. We use them to run tests in CI, validate PRs are in an expected state, and automate processes.
- [Overview of GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions)
- [What's a workflow?](https://docs.github.com/en/actions/using-workflows/about-workflows)
- [GitHub Actions guides](https://docs.github.com/en/actions/guides)
___
## Where do actions and workflows live
We try to maintain actions that are shared across repositories in a single place so that necesary changes can be made in a single place.
[dbt-labs/actions](https://github.com/dbt-labs/actions/) is the central repository of actions and workflows we use across repositories.
GitHub Actions also live locally within a repository. The workflows can be found at `.github/workflows` from the root of the repository. These should be specific to that code base.
Note: We are actively moving actions into the central Action repository so there is currently some duplication across repositories.
___
## Basics of Using Actions
### Viewing Output
- View the detailed action output for your PR in the **Checks** tab of the PR. This only shows the most recent run. You can also view high level **Checks** output at the bottom on the PR.
- View _all_ action output for a repository from the [**Actions**](https://github.com/dbt-labs/dbt-core/actions) tab. Workflow results last 1 year. Artifacts last 90 days, unless specified otherwise in individual workflows.
This view often shows what seem like duplicates of the same workflow. This occurs when files are renamed but the workflow name has not changed. These are in fact _not_ duplicates.
You can see the branch the workflow runs from in this view. It is listed in the table between the workflow name and the time/duration of the run. When blank, the workflow is running in the context of the `main` branch.
### How to view what workflow file is being referenced from a run
- When viewing the output of a specific workflow run, click the 3 dots at the top right of the display. There will be an option to `View workflow file`.
### How to manually run a workflow
- If a workflow has the `on: workflow_dispatch` trigger, it can be manually triggered
- From the [**Actions**](https://github.com/dbt-labs/dbt-core/actions) tab, find the workflow you want to run, select it and fill in any inputs requied. That's it!
### How to re-run jobs
- Some actions cannot be rerun in the GitHub UI. Namely the snyk checks and the cla check. Snyk checks are rerun by closing and reopening the PR. You can retrigger the cla check by commenting on the PR with `@cla-bot check`
___
## General Standards
### Permissions
- By default, workflows have read permissions in the repository for the contents scope only when no permissions are explicitly set.
- It is best practice to always define the permissions explicitly. This will allow actions to continue to work when the default permissions on the repository are changed. It also allows explicit grants of the least permissions possible.
- There are a lot of permissions available. [Read up on them](https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs) if you're unsure what to use.
```yaml
permissions:
contents: read
pull-requests: write
```
### Secrets
- When to use a [Personal Access Token (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) vs the [GITHUB_TOKEN](https://docs.github.com/en/actions/security-guides/automatic-token-authentication) generated for the action?
The `GITHUB_TOKEN` is used by default. In most cases it is sufficient for what you need.
If you expect the workflow to result in a commit to that should retrigger workflows, you will need to use a Personal Access Token for the bot to commit the file. When using the GITHUB_TOKEN, the resulting commit will not trigger another GitHub Actions Workflow run. This is due to limitations set by GitHub. See [the docs](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) for a more detailed explanation.
For example, we must use a PAT in our workflow to commit a new changelog yaml file for bot PRs. Once the file has been committed to the branch, it should retrigger the check to validate that a changelog exists on the PR. Otherwise, it would stay in a failed state since the check would never retrigger.
### Triggers
You can configure your workflows to run when specific activity on GitHub happens, at a scheduled time, or when an event outside of GitHub occurs. Read more details in the [GitHub docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows).
These triggers are under the `on` key of the workflow and more than one can be listed.
```yaml
on:
push:
branches:
- "main"
- "*.latest"
- "releases/*"
pull_request:
# catch when the PR is opened with the label or when the label is added
types: [opened, labeled]
workflow_dispatch:
```
Some triggers of note that we use:
- `push` - Runs your workflow when you push a commit or tag.
- `pull_request` - Runs your workflow when activity on a pull request in the workflow's repository occurs. Takes in a list of activity types (opened, labeled, etc) if appropriate.
- `pull_request_target` - Same as `pull_request` but runs in the context of the PR target branch.
- `workflow_call` - used with reusable workflows. Triggered by another workflow calling it.
- `workflow_dispatch` - Gives the ability to manually trigger a workflow from the GitHub API, GitHub CLI, or GitHub browser interface.
### Basic Formatting
- Add a description of what your workflow does at the top in this format
```
# **what?**
# Describe what the action does.
# **why?**
# Why does this action exist?
# **when?**
# How/when will it be triggered?
```
- Leave blank lines between steps and jobs
```yaml
jobs:
dependency_changelog:
runs-on: ubuntu-latest
steps:
- name: Get File Name Timestamp
id: filename_time
uses: nanzm/get-time-action@v1.1
with:
format: 'YYYYMMDD-HHmmss'
- name: Get File Content Timestamp
id: file_content_time
uses: nanzm/get-time-action@v1.1
with:
format: 'YYYY-MM-DDTHH:mm:ss.000000-05:00'
- name: Generate Filepath
id: fp
run: |
FILEPATH=.changes/unreleased/Dependencies-${{ steps.filename_time.outputs.time }}.yaml
echo "FILEPATH=$FILEPATH" >> $GITHUB_OUTPUT
```
- Print out all variables you will reference as the first step of a job. This allows for easier debugging. The first job should log all inputs. Subsequent jobs should reference outputs of other jobs, if present.
When possible, generate variables at the top of your workflow in a single place to reference later. This is not always strictly possible since you may generate a value to be used later mid-workflow.
Be sure to use quotes around these logs so special characters are not interpreted.
```yaml
job1:
- name: "[DEBUG] Print Variables"
run: |
echo "all variables defined as inputs"
echo "The last commit sha in the release: ${{ inputs.sha }}"
echo "The release version number: ${{ inputs.version_number }}"
echo "The changelog_path: ${{ inputs.changelog_path }}"
echo "The build_script_path: ${{ inputs.build_script_path }}"
echo "The s3_bucket_name: ${{ inputs.s3_bucket_name }}"
echo "The package_test_command: ${{ inputs.package_test_command }}"
# collect all the variables that need to be used in subsequent jobs
- name: Set Variables
id: variables
run: |
echo "important_path='performance/runner/Cargo.toml'" >> $GITHUB_OUTPUT
echo "release_id=${{github.event.inputs.release_id}}" >> $GITHUB_OUTPUT
echo "open_prs=${{github.event.inputs.open_prs}}" >> $GITHUB_OUTPUT
job2:
needs: [job1]
- name: "[DEBUG] Print Variables"
run: |
echo "all variables defined in job1 > Set Variables > outputs"
echo "important_path: ${{ needs.job1.outputs.important_path }}"
echo "release_id: ${{ needs.job1.outputs.release_id }}"
echo "open_prs: ${{ needs.job1.outputs.open_prs }}"
```
- When it's not obvious what something does, add a comment!
___
## Tips
### Context
- The [GitHub CLI](https://cli.github.com/) is available in the default runners
- Actions run in your context. ie, using an action from the marketplace that uses the GITHUB_TOKEN uses the GITHUB_TOKEN generated by your workflow run.
### Actions from the Marketplace
- Dont use external actions for things that can easily be accomplished manually.
- Always read through what an external action does before using it! Often an action in the GitHub Actions Marketplace can be replaced with a few lines in bash. This is much more maintainable (and wont change under us) and clear as to whats actually happening. It also prevents any
- Pin actions _we don't control_ to tags.
### Connecting to AWS
- Authenticate with the aws managed workflow
```yaml
- name: Configure AWS credentials from Test account
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
```
- Then access with the aws command that comes installed on the action runner machines
```yaml
- name: Copy Artifacts from S3 via CLI
run: aws s3 cp ${{ env.s3_bucket }} . --recursive
```
### Testing
- Depending on what your action does, you may be able to use [`act`](https://github.com/nektos/act) to test the action locally. Some features of GitHub Actions do not work with `act`, among those are reusable workflows. If you can't use `act`, you'll have to push your changes up before being able to test. This can be slow.

View File

@@ -28,11 +28,10 @@ if __name__ == "__main__":
if package_request.status_code == 404: if package_request.status_code == 404:
if halt_on_missing: if halt_on_missing:
sys.exit(1) sys.exit(1)
else:
# everything is the latest if the package doesn't exist # everything is the latest if the package doesn't exist
github_output = os.environ.get("GITHUB_OUTPUT") print(f"::set-output name=latest::{True}")
with open(github_output, "at", encoding="utf-8") as gh_output: print(f"::set-output name=minor_latest::{True}")
gh_output.write("latest=True")
gh_output.write("minor_latest=True")
sys.exit(0) sys.exit(0)
# TODO: verify package meta is "correct" # TODO: verify package meta is "correct"
@@ -92,7 +91,5 @@ if __name__ == "__main__":
latest = is_latest(pre_rel, new_version, current_latest) latest = is_latest(pre_rel, new_version, current_latest)
minor_latest = is_latest(pre_rel, new_version, current_minor_latest) minor_latest = is_latest(pre_rel, new_version, current_minor_latest)
github_output = os.environ.get("GITHUB_OUTPUT") print(f"::set-output name=latest::{latest}")
with open(github_output, "at", encoding="utf-8") as gh_output: print(f"::set-output name=minor_latest::{minor_latest}")
gh_output.write(f"latest={latest}")
gh_output.write(f"minor_latest={minor_latest}")

View File

@@ -15,9 +15,7 @@ resolves #
### Checklist ### Checklist
- [ ] I have read [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md) and understand what's expected of me
- [ ] I have signed the [CLA](https://docs.getdbt.com/docs/contributor-license-agreements) - [ ] I have signed the [CLA](https://docs.getdbt.com/docs/contributor-license-agreements)
- [ ] I have run this code in development and it appears to resolve the stated issue - [ ] I have run this code in development and it appears to resolve the stated issue
- [ ] This PR includes tests, or tests are not required/relevant for this PR - [ ] This PR includes tests, or tests are not required/relevant for this PR
- [ ] I have [opened an issue to add/update docs](https://github.com/dbt-labs/docs.getdbt.com/issues/new/choose), or docs changes are not required/relevant for this PR - [ ] I have added information about my change to be included in the [CHANGELOG](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#Adding-CHANGELOG-Entry).
- [ ] I have run `changie new` to [create a changelog entry](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#adding-a-changelog-entry)

View File

@@ -13,28 +13,22 @@
# This automates the backporting process # This automates the backporting process
# **when?** # **when?**
# Once a PR is "Squash and merge"'d, by adding a backport label, this is triggered # Once a PR is "Squash and merge"'d and it has been correctly labeled
# according to the naming convention.
name: Backport name: Backport
on: on:
pull_request: pull_request:
types: types:
- closed
- labeled - labeled
permissions:
contents: write
pull-requests: write
jobs: jobs:
backport: backport:
runs-on: ubuntu-18.04
name: Backport name: Backport
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& contains(github.event.label.name, 'backport')
steps: steps:
- uses: tibdex/backport@v2.0.2 - name: Backport
uses: dbt-labs/backport@v1.1.1
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,61 +0,0 @@
# **what?**
# When bots create a PR, this action will add a corresponding changie yaml file to that
# PR when a specific label is added.
#
# The file is created off a template:
#
# kind: <per action matrix>
# body: <PR title>
# time: <current timestamp>
# custom:
# Author: <PR User Login (generally the bot)>
# Issue: 4904
# PR: <PR number>
#
# **why?**
# Automate changelog generation for more visability with automated bot PRs.
#
# **when?**
# Once a PR is created, label should be added to PR before or after creation. You can also
# manually trigger this by adding the appropriate label at any time.
#
# **how to add another bot?**
# Add the label and changie kind to the include matrix. That's it!
#
name: Bot Changelog
on:
pull_request:
# catch when the PR is opened with the label or when the label is added
types: [labeled]
permissions:
contents: write
pull-requests: read
jobs:
generate_changelog:
strategy:
matrix:
include:
- label: "dependencies"
changie_kind: "Dependencies"
- label: "snyk"
changie_kind: "Security"
runs-on: ubuntu-latest
steps:
- name: Create and commit changelog on bot PR
if: ${{ contains(github.event.pull_request.labels.*.name, matrix.label) }}
id: bot_changelog
uses: emmyoop/changie_bot@v1.0.1
with:
GITHUB_TOKEN: ${{ secrets.FISHTOWN_BOT_PAT }}
commit_author_name: "Github Build Bot"
commit_author_email: "<buildbot@fishtownanalytics.com>"
commit_message: "Add automated changelog yaml from template for bot PR"
changie_kind: ${{ matrix.changie_kind }}
label: ${{ matrix.label }}
custom_changelog_string: "custom:\n Author: ${{ github.event.pull_request.user.login }}\n PR: ${{ github.event.pull_request.number }}"

78
.github/workflows/changelog-check.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
# **what?**
# Checks that a file has been committed under the /.changes directory
# as a new CHANGELOG entry. Cannot check for a specific filename as
# it is dynamically generated by change type and timestamp.
# This workflow should not require any secrets since it runs for PRs
# from forked repos.
# By default, secrets are not passed to workflows running from
# a forked repo.
# **why?**
# Ensure code change gets reflected in the CHANGELOG.
# **when?**
# This will run for all PRs going into main and *.latest. It will
# run when they are opened, reopened, when any label is added or removed
# and when new code is pushed to the branch. The action will then get
# skipped if the 'Skip Changelog' label is present is any of the labels.
name: Check Changelog Entry
on:
pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize]
workflow_dispatch:
defaults:
run:
shell: bash
permissions:
contents: read
pull-requests: write
env:
changelog_comment: 'Thank you for your pull request! We could not find a changelog entry for this change. For details on how to document a change, see [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#adding-changelog-entry).'
jobs:
changelog:
name: changelog
if: "!contains(github.event.pull_request.labels.*.name, 'Skip Changelog')"
runs-on: ubuntu-latest
steps:
- name: Check if changelog file was added
# https://github.com/marketplace/actions/paths-changes-filter
# For each filter, it sets output variable named by the filter to the text:
# 'true' - if any of changed files matches any of filter rules
# 'false' - if none of changed files matches any of filter rules
# also, returns:
# `changes` - JSON array with names of all filters matching any of the changed files
uses: dorny/paths-filter@v2
id: filter
with:
token: ${{ secrets.GITHUB_TOKEN }}
filters: |
changelog:
- added: '.changes/unreleased/**.yaml'
- name: Check if comment already exists
uses: peter-evans/find-comment@v1
id: changelog_comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: ${{ env.changelog_comment }}
- name: Create PR comment if changelog entry is missing, required, and does nto exist
if: |
steps.filter.outputs.changelog == 'false' &&
steps.changelog_comment.outputs.comment-body == ''
uses: peter-evans/create-or-update-comment@v1
with:
issue-number: ${{ github.event.pull_request.number }}
body: ${{ env.changelog_comment }}
- name: Fail job if changelog entry is missing and required
if: steps.filter.outputs.changelog == 'false'
uses: actions/github-script@v6
with:
script: core.setFailed('Changelog entry required to merge.')

View File

@@ -1,40 +0,0 @@
# **what?**
# Checks that a file has been committed under the /.changes directory
# as a new CHANGELOG entry. Cannot check for a specific filename as
# it is dynamically generated by change type and timestamp.
# This workflow should not require any secrets since it runs for PRs
# from forked repos.
# By default, secrets are not passed to workflows running from
# a forked repo.
# **why?**
# Ensure code change gets reflected in the CHANGELOG.
# **when?**
# This will run for all PRs going into main and *.latest. It will
# run when they are opened, reopened, when any label is added or removed
# and when new code is pushed to the branch. The action will then get
# skipped if the 'Skip Changelog' label is present is any of the labels.
name: Check Changelog Entry
on:
pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize]
workflow_dispatch:
defaults:
run:
shell: bash
permissions:
contents: read
pull-requests: write
jobs:
changelog:
uses: dbt-labs/actions/.github/workflows/changelog-existence.yml@main
with:
changelog_comment: 'Thank you for your pull request! We could not find a changelog entry for this change. For details on how to document a change, see [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#adding-changelog-entry).'
skip_label: 'Skip Changelog'
secrets: inherit

View File

@@ -1,165 +0,0 @@
# **what?**
# On push, if anything in core/dbt/docs or core/dbt/cli has been
# created or modified, regenerate the CLI API docs using sphinx.
# **why?**
# We watch for changes in core/dbt/cli because the CLI API docs rely on click
# and all supporting flags/params to be generated. We watch for changes in
# core/dbt/docs since any changes to sphinx configuration or any of the
# .rst files there could result in a differently build final index.html file.
# **when?**
# Whenever a change has been pushed to a branch, and only if there is a diff
# between the PR branch and main's core/dbt/cli and or core/dbt/docs dirs.
# TODO: add bot comment to PR informing contributor that the docs have been committed
# TODO: figure out why github action triggered pushes cause github to fail to report
# the status of jobs
name: Generate CLI API docs
on:
pull_request:
permissions:
contents: write
pull-requests: write
env:
CLI_DIR: ${{ github.workspace }}/core/dbt/cli
DOCS_DIR: ${{ github.workspace }}/core/dbt/docs
DOCS_BUILD_DIR: ${{ github.workspace }}/core/dbt/docs/build
jobs:
check_gen:
name: check if generation needed
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.head.repo.fork == false }}
outputs:
cli_dir_changed: ${{ steps.check_cli.outputs.cli_dir_changed }}
docs_dir_changed: ${{ steps.check_docs.outputs.docs_dir_changed }}
steps:
- name: "[DEBUG] print variables"
run: |
echo "env.CLI_DIR: ${{ env.CLI_DIR }}"
echo "env.DOCS_BUILD_DIR: ${{ env.DOCS_BUILD_DIR }}"
echo "env.DOCS_DIR: ${{ env.DOCS_DIR }}"
- name: git checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
- name: set shas
id: set_shas
run: |
THIS_SHA=$(git rev-parse @)
LAST_SHA=$(git rev-parse @~1)
echo "this sha: $THIS_SHA"
echo "last sha: $LAST_SHA"
echo "this_sha=$THIS_SHA" >> $GITHUB_OUTPUT
echo "last_sha=$LAST_SHA" >> $GITHUB_OUTPUT
- name: check for changes in core/dbt/cli
id: check_cli
run: |
CLI_DIR_CHANGES=$(git diff \
${{ steps.set_shas.outputs.last_sha }} \
${{ steps.set_shas.outputs.this_sha }} \
-- ${{ env.CLI_DIR }})
if [ -n "$CLI_DIR_CHANGES" ]; then
echo "changes found"
echo $CLI_DIR_CHANGES
echo "cli_dir_changed=true" >> $GITHUB_OUTPUT
exit 0
fi
echo "cli_dir_changed=false" >> $GITHUB_OUTPUT
echo "no changes found"
- name: check for changes in core/dbt/docs
id: check_docs
if: steps.check_cli.outputs.cli_dir_changed == 'false'
run: |
DOCS_DIR_CHANGES=$(git diff --name-only \
${{ steps.set_shas.outputs.last_sha }} \
${{ steps.set_shas.outputs.this_sha }} \
-- ${{ env.DOCS_DIR }} ':!${{ env.DOCS_BUILD_DIR }}')
DOCS_BUILD_DIR_CHANGES=$(git diff --name-only \
${{ steps.set_shas.outputs.last_sha }} \
${{ steps.set_shas.outputs.this_sha }} \
-- ${{ env.DOCS_BUILD_DIR }})
if [ -n "$DOCS_DIR_CHANGES" ] && [ -z "$DOCS_BUILD_DIR_CHANGES" ]; then
echo "changes found"
echo $DOCS_DIR_CHANGES
echo "docs_dir_changed=true" >> $GITHUB_OUTPUT
exit 0
fi
echo "docs_dir_changed=false" >> $GITHUB_OUTPUT
echo "no changes found"
gen_docs:
name: generate docs
runs-on: ubuntu-latest
needs: [check_gen]
if: |
needs.check_gen.outputs.cli_dir_changed == 'true'
|| needs.check_gen.outputs.docs_dir_changed == 'true'
steps:
- name: "[DEBUG] print variables"
run: |
echo "env.DOCS_DIR: ${{ env.DOCS_DIR }}"
echo "github head_ref: ${{ github.head_ref }}"
- name: git checkout
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
- name: install python
uses: actions/setup-python@v4.3.0
with:
python-version: 3.8
- name: install dev requirements
run: |
python3 -m venv env
source env/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txt -r dev-requirements.txt
- name: generate docs
run: |
source env/bin/activate
cd ${{ env.DOCS_DIR }}
echo "cleaning existing docs"
make clean
echo "creating docs"
make html
- name: debug
run: |
echo ">>>>> status"
git status
echo ">>>>> remotes"
git remote -v
echo ">>>>> branch"
git branch -v
echo ">>>>> log"
git log --pretty=oneline | head -5
- name: commit docs
run: |
git config user.name 'Github Build Bot'
git config user.email 'buildbot@fishtownanalytics.com'
git commit -am "Add generated CLI API docs"
git push -u origin ${{ github.head_ref }}

View File

@@ -15,9 +15,6 @@ on:
issues: issues:
types: [closed, deleted, reopened] types: [closed, deleted, reopened]
# no special access is needed
permissions: read-all
jobs: jobs:
call-label-action: call-label-action:
uses: dbt-labs/jira-actions/.github/workflows/jira-transition.yml@main uses: dbt-labs/jira-actions/.github/workflows/jira-transition.yml@main

View File

@@ -38,27 +38,23 @@ jobs:
name: code-quality name: code-quality
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Install python dependencies - name: Install python dependencies
run: | run: |
python -m pip install --user --upgrade pip pip install --user --upgrade pip
python -m pip --version pip --version
python -m pip install pre-commit pip install pre-commit
pre-commit --version pre-commit --version
python -m pip install mypy==0.942 pip install mypy==0.782
mypy --version mypy --version
python -m pip install -r requirements.txt pip install -r editable-requirements.txt
python -m pip install -r dev-requirements.txt
dbt --version dbt --version
- name: Run pre-commit hooks - name: Run pre-commit hooks
@@ -68,12 +64,11 @@ jobs:
name: unit test / python ${{ matrix.python-version }} name: unit test / python ${{ matrix.python-version }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] python-version: ['3.7', '3.8', '3.9', '3.10']
env: env:
TOXENV: "unit" TOXENV: "unit"
@@ -84,15 +79,15 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install python dependencies - name: Install python dependencies
run: | run: |
python -m pip install --user --upgrade pip pip install --user --upgrade pip
python -m pip --version pip --version
python -m pip install tox pip install tox
tox --version tox --version
- name: Run tox - name: Run tox
@@ -101,9 +96,7 @@ jobs:
- name: Get current date - name: Get current date
if: always() if: always()
id: date id: date
run: | run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" #no colons allowed for artifacts
CURRENT_DATE=$(date +'%Y-%m-%dT%H_%M_%S') # no colons allowed for artifacts
echo "date=$CURRENT_DATE" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: always() if: always()
@@ -115,13 +108,12 @@ jobs:
name: integration test / python ${{ matrix.python-version }} / ${{ matrix.os }} name: integration test / python ${{ matrix.python-version }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] python-version: ['3.7', '3.8', '3.9', '3.10']
os: [ubuntu-20.04] os: [ubuntu-latest]
include: include:
- python-version: 3.8 - python-version: 3.8
os: windows-latest os: windows-latest
@@ -132,16 +124,13 @@ jobs:
TOXENV: integration TOXENV: integration
PYTEST_ADDOPTS: "-v --color=yes -n4 --csv integration_results.csv" PYTEST_ADDOPTS: "-v --color=yes -n4 --csv integration_results.csv"
DBT_INVOCATION_ENV: github-actions DBT_INVOCATION_ENV: github-actions
DBT_TEST_USER_1: dbt_test_user_1
DBT_TEST_USER_2: dbt_test_user_2
DBT_TEST_USER_3: dbt_test_user_3
steps: steps:
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@@ -159,9 +148,9 @@ jobs:
- name: Install python tools - name: Install python tools
run: | run: |
python -m pip install --user --upgrade pip pip install --user --upgrade pip
python -m pip --version pip --version
python -m pip install tox pip install tox
tox --version tox --version
- name: Run tests - name: Run tests
@@ -170,9 +159,7 @@ jobs:
- name: Get current date - name: Get current date
if: always() if: always()
id: date id: date
run: | run: echo "::set-output name=date::$(date +'%Y_%m_%dT%H_%M_%S')" #no colons allowed for artifacts
CURRENT_DATE=$(date +'%Y-%m-%dT%H_%M_%S') # no colons allowed for artifacts
echo "date=$CURRENT_DATE" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: always() if: always()
@@ -196,15 +183,15 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2
with: with:
python-version: '3.8' python-version: 3.8
- name: Install python dependencies - name: Install python dependencies
run: | run: |
python -m pip install --user --upgrade pip pip install --user --upgrade pip
python -m pip install --upgrade setuptools wheel twine check-wheel-contents pip install --upgrade setuptools wheel twine check-wheel-contents
python -m pip --version pip --version
- name: Build distributions - name: Build distributions
run: ./scripts/build-dist.sh run: ./scripts/build-dist.sh
@@ -222,7 +209,7 @@ jobs:
- name: Install wheel distributions - name: Install wheel distributions
run: | run: |
find ./dist/*.whl -maxdepth 1 -type f | xargs python -m pip install --force-reinstall --find-links=dist/ find ./dist/*.whl -maxdepth 1 -type f | xargs pip install --force-reinstall --find-links=dist/
- name: Check wheel distributions - name: Check wheel distributions
run: | run: |
@@ -231,7 +218,7 @@ jobs:
- name: Install source distributions - name: Install source distributions
# ignore dbt-1.0.0, which intentionally raises an error when installed from source # ignore dbt-1.0.0, which intentionally raises an error when installed from source
run: | run: |
find ./dist/dbt-[a-z]*.gz -maxdepth 1 -type f | xargs python -m pip install --force-reinstall --find-links=dist/ find ./dist/dbt-[a-z]*.gz -maxdepth 1 -type f | xargs pip install --force-reinstall --find-links=dist/
- name: Check source distributions - name: Check source distributions
run: | run: |

View File

@@ -1,62 +0,0 @@
# **what?**
# The purpose of this workflow is to trigger CI to run for each
# release branch and main branch on a regular cadence. If the CI workflow
# fails for a branch, it will post to dev-core-alerts to raise awareness.
# The 'aurelien-baudet/workflow-dispatch' Action triggers the existing
# CI worklow file on the given branch to run so that even if we change the
# CI workflow file in the future, the one that is tailored for the given
# release branch will be used.
# **why?**
# Ensures release branches and main are always shippable and not broken.
# Also, can catch any dependencies shifting beneath us that might
# introduce breaking changes (could also impact Cloud).
# **when?**
# Mainly on a schedule of 9:00, 13:00, 18:00 UTC everyday.
# Manual trigger can also test on demand
name: Release branch scheduled testing
on:
schedule:
- cron: '0 9,13,18 * * *' # 9:00, 13:00, 18:00 UTC
workflow_dispatch: # for manual triggering
# no special access is needed
permissions: read-all
jobs:
kick-off-ci:
name: Kick-off CI
runs-on: ubuntu-latest
strategy:
# must run CI 1 branch at a time b/c the workflow-dispatch Action polls for
# latest run for results and it gets confused when we kick off multiple runs
# at once. There is a race condition so we will just run in sequential order.
max-parallel: 1
fail-fast: false
matrix:
branch: [1.0.latest, 1.1.latest, 1.2.latest, 1.3.latest, main]
steps:
- name: Call CI workflow for ${{ matrix.branch }} branch
id: trigger-step
uses: aurelien-baudet/workflow-dispatch@v2.1.1
with:
workflow: main.yml
ref: ${{ matrix.branch }}
token: ${{ secrets.FISHTOWN_BOT_PAT }}
- name: Post failure to Slack
uses: ravsamhq/notify-slack-action@v1
if: ${{ always() && !contains(steps.trigger-step.outputs.workflow-conclusion,'success') }}
with:
status: ${{ job.status }}
notification_title: 'dbt-core scheduled run of "${{ matrix.branch }}" branch not successful'
message_format: ':x: CI on branch "${{ matrix.branch }}" ${{ steps.trigger-step.outputs.workflow-conclusion }}'
footer: 'Linked failed CI run ${{ steps.trigger-step.outputs.workflow-url }}'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DEV_CORE_ALERTS }}

View File

@@ -41,9 +41,9 @@ jobs:
id: version id: version
run: | run: |
IFS="." read -r MAJOR MINOR PATCH <<< ${{ github.event.inputs.version_number }} IFS="." read -r MAJOR MINOR PATCH <<< ${{ github.event.inputs.version_number }}
echo "major=$MAJOR" >> $GITHUB_OUTPUT echo "::set-output name=major::$MAJOR"
echo "minor=$MINOR" >> $GITHUB_OUTPUT echo "::set-output name=minor::$MINOR"
echo "patch=$PATCH" >> $GITHUB_OUTPUT echo "::set-output name=patch::$PATCH"
- name: Is pkg 'latest' - name: Is pkg 'latest'
id: latest id: latest
@@ -70,10 +70,8 @@ jobs:
- name: Get docker build arg - name: Get docker build arg
id: build_arg id: build_arg
run: | run: |
BUILD_ARG_NAME=$(echo ${{ github.event.inputs.package }} | sed 's/\-/_/g') echo "::set-output name=build_arg_name::"$(echo ${{ github.event.inputs.package }} | sed 's/\-/_/g')
BUILD_ARG_VALUE=$(echo ${{ github.event.inputs.package }} | sed 's/postgres/core/g') echo "::set-output name=build_arg_value::"$(echo ${{ github.event.inputs.package }} | sed 's/postgres/core/g')
echo "build_arg_name=$BUILD_ARG_NAME" >> $GITHUB_OUTPUT
echo "build_arg_value=$BUILD_ARG_VALUE" >> $GITHUB_OUTPUT
- name: Log in to the GHCR - name: Log in to the GHCR
uses: docker/login-action@v1 uses: docker/login-action@v1

View File

@@ -20,9 +20,6 @@ on:
description: 'The release version number (i.e. 1.0.0b1)' description: 'The release version number (i.e. 1.0.0b1)'
required: true required: true
permissions:
contents: write # this is the permission that allows creating a new release
defaults: defaults:
run: run:
shell: bash shell: bash
@@ -165,7 +162,7 @@ jobs:
env: env:
IS_PRERELEASE: ${{ contains(github.event.inputs.version_number, 'rc') || contains(github.event.inputs.version_number, 'b') }} IS_PRERELEASE: ${{ contains(github.event.inputs.version_number, 'rc') || contains(github.event.inputs.version_number, 'b') }}
run: | run: |
echo "isPrerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT echo ::set-output name=isPrerelease::$IS_PRERELEASE
- name: Creating GitHub Release - name: Creating GitHub Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1

View File

@@ -21,9 +21,6 @@ on:
- "*.latest" - "*.latest"
- "releases/*" - "releases/*"
# no special access is needed
permissions: read-all
env: env:
LATEST_SCHEMA_PATH: ${{ github.workspace }}/new_schemas LATEST_SCHEMA_PATH: ${{ github.workspace }}/new_schemas
SCHEMA_DIFF_ARTIFACT: ${{ github.workspace }}//schema_schanges.txt SCHEMA_DIFF_ARTIFACT: ${{ github.workspace }}//schema_schanges.txt

View File

@@ -3,10 +3,16 @@ on:
schedule: schedule:
- cron: "30 1 * * *" - cron: "30 1 * * *"
permissions:
issues: write
pull-requests: write
jobs: jobs:
stale: stale:
uses: dbt-labs/actions/.github/workflows/stale-bot-matrix.yml@main runs-on: ubuntu-latest
steps:
# pinned at v4 (https://github.com/actions/stale/releases/tag/v4.0.0)
- uses: actions/stale@cdf15f641adb27a71842045a94023bef6945e3aa
with:
stale-issue-message: "This issue has been marked as Stale because it has been open for 180 days with no activity. If you would like the issue to remain open, please remove the stale label or comment on the issue, or it will be closed in 7 days."
stale-pr-message: "This PR has been marked as Stale because it has been open for 180 days with no activity. If you would like the PR to remain open, please remove the stale label or comment on the PR, or it will be closed in 7 days."
# mark issues/PRs stale when they haven't seen activity in 180 days
days-before-stale: 180
# ignore checking issues with the following labels
exempt-issue-labels: "epic,discussion"

View File

@@ -22,7 +22,7 @@ jobs:
# run the performance measurements on the current or default branch # run the performance measurements on the current or default branch
test-schema: test-schema:
name: Test Log Schema name: Test Log Schema
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
env: env:
# turns warnings into errors # turns warnings into errors
RUSTFLAGS: "-D warnings" RUSTFLAGS: "-D warnings"
@@ -30,11 +30,6 @@ jobs:
LOG_DIR: "/home/runner/work/dbt-core/dbt-core/logs" LOG_DIR: "/home/runner/work/dbt-core/dbt-core/logs"
# tells integration tests to output into json format # tells integration tests to output into json format
DBT_LOG_FORMAT: "json" DBT_LOG_FORMAT: "json"
# Additional test users
DBT_TEST_USER_1: dbt_test_user_1
DBT_TEST_USER_2: dbt_test_user_2
DBT_TEST_USER_3: dbt_test_user_3
steps: steps:
- name: checkout dev - name: checkout dev
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -46,6 +41,12 @@ jobs:
with: with:
python-version: "3.8" python-version: "3.8"
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Install python dependencies - name: Install python dependencies
run: | run: |
pip install --user --upgrade pip pip install --user --upgrade pip
@@ -63,3 +64,10 @@ jobs:
# we actually care if these pass, because the normal test run doesn't usually include many json log outputs # we actually care if these pass, because the normal test run doesn't usually include many json log outputs
- name: Run integration tests - name: Run integration tests
run: tox -e integration -- -nauto run: tox -e integration -- -nauto
# apply our schema tests to every log event from the previous step
# skips any output that isn't valid json
- uses: actions-rs/cargo@v1
with:
command: run
args: --manifest-path test/interop/log_parsing/Cargo.toml

View File

@@ -1,33 +0,0 @@
# **what?**
# When the core team triages, we sometimes need more information from the issue creator. In
# those cases we remove the `triage` label and add the `awaiting_response` label. Once we
# recieve a response in the form of a comment, we want the `awaiting_response` label removed
# in favor of the `triage` label so we are aware that the issue needs action.
# **why?**
# To help with out team triage issue tracking
# **when?**
# This will run when a comment is added to an issue and that issue has to `awaiting_response` label.
name: Update Triage Label
on: issue_comment
defaults:
run:
shell: bash
permissions:
issues: write
jobs:
triage_label:
if: contains(github.event.issue.labels.*.name, 'awaiting_response')
runs-on: ubuntu-latest
steps:
- name: initial labeling
uses: andymckay/labeler@master
with:
add-labels: "triage"
remove-labels: "awaiting_response"

View File

@@ -1,15 +1,18 @@
# **what?** # **what?**
# This workflow will take the new version number to bump to. With that # This workflow will take a version number and a dry run flag. With that
# it will run versionbump to update the version number everywhere in the # it will run versionbump to update the version number everywhere in the
# code base and then run changie to create the corresponding changelog. # code base and then generate an update Docker requirements file. If this
# A PR will be created with the changes that can be reviewed before committing. # is a dry run, a draft PR will open with the changes. If this isn't a dry
# run, the changes will be committed to the branch this is run on.
# **why?** # **why?**
# This is to aid in releasing dbt and making sure we have updated # This is to aid in releasing dbt and making sure we have updated
# the version in all places and generated the changelog. # the versions and Docker requirements in all places.
# **when?** # **when?**
# This is triggered manually # This is triggered either manually OR
# from the repository_dispatch event "version-bump" which is sent from
# the dbt-release repo Action
name: Version Bump name: Version Bump
@@ -17,25 +20,35 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version_number: version_number:
description: 'The version number to bump to (ex. 1.2.0, 1.3.0b1)' description: 'The version number to bump to'
required: true required: true
is_dry_run:
permissions: description: 'Creates a draft PR to allow testing instead of committing to a branch'
contents: write required: true
pull-requests: write default: 'true'
repository_dispatch:
types: [version-bump]
jobs: jobs:
bump: bump:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: "[DEBUG] Print Variables"
run: |
echo "all variables defined as inputs"
echo The version_number: ${{ github.event.inputs.version_number }}
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set version and dry run values
id: variables
env:
VERSION_NUMBER: "${{ github.event.client_payload.version_number == '' && github.event.inputs.version_number || github.event.client_payload.version_number }}"
IS_DRY_RUN: "${{ github.event.client_payload.is_dry_run == '' && github.event.inputs.is_dry_run || github.event.client_payload.is_dry_run }}"
run: |
echo Repository dispatch event version: ${{ github.event.client_payload.version_number }}
echo Repository dispatch event dry run: ${{ github.event.client_payload.is_dry_run }}
echo Workflow dispatch event version: ${{ github.event.inputs.version_number }}
echo Workflow dispatch event dry run: ${{ github.event.inputs.is_dry_run }}
echo ::set-output name=VERSION_NUMBER::$VERSION_NUMBER
echo ::set-output name=IS_DRY_RUN::$IS_DRY_RUN
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: "3.8" python-version: "3.8"
@@ -46,80 +59,51 @@ jobs:
source env/bin/activate source env/bin/activate
pip install --upgrade pip pip install --upgrade pip
- name: Add Homebrew to PATH
run: |
echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH
- name: Install Homebrew packages
run: |
brew install pre-commit
brew tap miniscruff/changie https://github.com/miniscruff/changie
brew install changie
- name: Audit Version and Parse Into Parts
id: semver
uses: dbt-labs/actions/parse-semver@v1
with:
version: ${{ github.event.inputs.version_number }}
- name: Set branch value
id: variables
run: |
echo "BRANCH_NAME=prep-release/${{ github.event.inputs.version_number }}_$GITHUB_RUN_ID" >> $GITHUB_OUTPUT
- name: Create PR branch - name: Create PR branch
if: ${{ steps.variables.outputs.IS_DRY_RUN == 'true' }}
run: | run: |
git checkout -b ${{ steps.variables.outputs.BRANCH_NAME }} git checkout -b bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_$GITHUB_RUN_ID
git push origin ${{ steps.variables.outputs.BRANCH_NAME }} git push origin bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_$GITHUB_RUN_ID
git branch --set-upstream-to=origin/${{ steps.variables.outputs.BRANCH_NAME }} ${{ steps.variables.outputs.BRANCH_NAME }} git branch --set-upstream-to=origin/bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_$GITHUB_RUN_ID bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_$GITHUB_RUN_ID
# - name: Generate Docker requirements
# run: |
# source env/bin/activate
# pip install -r requirements.txt
# pip freeze -l > docker/requirements/requirements.txt
# git status
- name: Bump version - name: Bump version
run: | run: |
source env/bin/activate source env/bin/activate
pip install -r dev-requirements.txt pip install -r dev-requirements.txt
env/bin/bumpversion --allow-dirty --new-version ${{ github.event.inputs.version_number }} major env/bin/bumpversion --allow-dirty --new-version ${{steps.variables.outputs.VERSION_NUMBER}} major
git status git status
- name: Run changie - name: Commit version bump directly
run: |
if [[ ${{ steps.semver.outputs.is-pre-release }} -eq 1 ]]
then
changie batch ${{ steps.semver.outputs.base-version }} --move-dir '${{ steps.semver.outputs.base-version }}' --prerelease '${{ steps.semver.outputs.pre-release }}'
else
changie batch ${{ steps.semver.outputs.base-version }} --include '${{ steps.semver.outputs.base-version }}' --remove-prereleases
fi
changie merge
git status
# this step will fail on whitespace errors but also correct them
- name: Remove trailing whitespace
continue-on-error: true
run: |
pre-commit run trailing-whitespace --files .bumpversion.cfg CHANGELOG.md .changes/*
git status
# this step will fail on newline errors but also correct them
- name: Removing extra newlines
continue-on-error: true
run: |
pre-commit run end-of-file-fixer --files .bumpversion.cfg CHANGELOG.md .changes/*
git status
- name: Commit version bump to branch
uses: EndBug/add-and-commit@v7 uses: EndBug/add-and-commit@v7
if: ${{ steps.variables.outputs.IS_DRY_RUN == 'false' }}
with: with:
author_name: 'Github Build Bot' author_name: 'Github Build Bot'
author_email: 'buildbot@fishtownanalytics.com' author_email: 'buildbot@fishtownanalytics.com'
message: 'Bumping version to ${{ github.event.inputs.version_number }} and generate CHANGELOG' message: 'Bumping version to ${{steps.variables.outputs.VERSION_NUMBER}}'
branch: '${{ steps.variables.outputs.BRANCH_NAME }}'
push: 'origin origin/${{ steps.variables.outputs.BRANCH_NAME }}' - name: Commit version bump to branch
uses: EndBug/add-and-commit@v7
if: ${{ steps.variables.outputs.IS_DRY_RUN == 'true' }}
with:
author_name: 'Github Build Bot'
author_email: 'buildbot@fishtownanalytics.com'
message: 'Bumping version to ${{steps.variables.outputs.VERSION_NUMBER}}'
branch: 'bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_${{GITHUB.RUN_ID}}'
push: 'origin origin/bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_${{GITHUB.RUN_ID}}'
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v3 uses: peter-evans/create-pull-request@v3
if: ${{ steps.variables.outputs.IS_DRY_RUN == 'true' }}
with: with:
author: 'Github Build Bot <buildbot@fishtownanalytics.com>' author: 'Github Build Bot <buildbot@fishtownanalytics.com>'
draft: true
base: ${{github.ref}} base: ${{github.ref}}
title: 'Bumping version to ${{ github.event.inputs.version_number }} and generate changelog' title: 'Bumping version to ${{steps.variables.outputs.VERSION_NUMBER}}'
branch: '${{ steps.variables.outputs.BRANCH_NAME }}' branch: 'bumping-version/${{steps.variables.outputs.VERSION_NUMBER}}_${{GITHUB.RUN_ID}}'
labels: |
Skip Changelog

8
.gitignore vendored
View File

@@ -11,7 +11,6 @@ __pycache__/
env*/ env*/
dbt_env/ dbt_env/
build/ build/
!core/dbt/docs/build
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
@@ -25,8 +24,7 @@ var/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
.mypy_cache/ *.mypy_cache/
.dmypy.json
logs/ logs/
# PyInstaller # PyInstaller
@@ -97,7 +95,3 @@ venv/
# vscode # vscode
.vscode/ .vscode/
*.code-workspace
# poetry
poetry.lock

View File

@@ -2,11 +2,11 @@
# Eventually the hooks described here will be run as tests before merging each PR. # Eventually the hooks described here will be run as tests before merging each PR.
# TODO: remove global exclusion of tests when testing overhaul is complete # TODO: remove global exclusion of tests when testing overhaul is complete
exclude: ^(test/|core/dbt/docs/build/) exclude: ^test/
# Force all unspecified python hooks to run python 3.8 # Force all unspecified python hooks to run python 3.8
default_language_version: default_language_version:
python: python3 python: python3.8
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
@@ -24,13 +24,18 @@ repos:
rev: 22.3.0 rev: 22.3.0
hooks: hooks:
- id: black - id: black
args:
- "--line-length=99"
- "--target-version=py38"
- id: black - id: black
alias: black-check alias: black-check
stages: [manual] stages: [manual]
args: args:
- "--line-length=99"
- "--target-version=py38"
- "--check" - "--check"
- "--diff" - "--diff"
- repo: https://github.com/pycqa/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: 4.0.1 rev: 4.0.1
hooks: hooks:
- id: flake8 - id: flake8
@@ -38,7 +43,7 @@ repos:
alias: flake8-check alias: flake8-check
stages: [manual] stages: [manual]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.942 rev: v0.782
hooks: hooks:
- id: mypy - id: mypy
# N.B.: Mypy is... a bit fragile. # N.B.: Mypy is... a bit fragile.

View File

@@ -5,15 +5,109 @@
- "Breaking changes" listed under a version may require action from end users or external maintainers when upgrading to that version. - "Breaking changes" listed under a version may require action from end users or external maintainers when upgrading to that version.
- Do not edit this file directly. This file is auto-generated using [changie](https://github.com/miniscruff/changie). For details on how to document a change, see [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#adding-changelog-entry) - Do not edit this file directly. This file is auto-generated using [changie](https://github.com/miniscruff/changie). For details on how to document a change, see [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#adding-changelog-entry)
## dbt-core 1.1.0 - April 28, 2022
### Breaking Changes
- **Relevant to maintainers of adapter plugins _only_:** The abstractmethods `get_response` and `execute` now only return a `connection.AdapterReponse` in type hints. (Previously, they could return a string.) We encourage you to update your methods to return an object of class `AdapterResponse`, or implement a subclass specific to your adapter
([#4499](https://github.com/dbt-labs/dbt-core/issues/4499), [#4869](https://github.com/dbt-labs/dbt-core/pull/4869))
- For adapter plugin maintainers only: Internal adapter methods `set_relations_cache` + `_relations_cache_for_schemas` each take an additional argument, for use with experimental `CACHE_SELECTED_ONLY` config ([#4688](https://github.com/dbt-labs/dbt-core/issues/4688), [#4860](https://github.com/dbt-labs/dbt-core/pull/4860))
### Features
- Change behaviour of `non_null` test so that it only `select`s all columns if `--store-failures` is enabled. ([#4769](https://github.com/dbt-labs/dbt-core/issues/4769), [#4777](https://github.com/dbt-labs/dbt-core/pull/4777))
- Testing framework for dbt adapter testing ([#4730](https://github.com/dbt-labs/dbt-core/issues/4730), [#4846](https://github.com/dbt-labs/dbt-core/pull/4846))
- Allow unique key to take a list implementation for postgres/redshift ([#4738](https://github.com/dbt-labs/dbt-core/issues/4738), [#4858](https://github.com/dbt-labs/dbt-core/pull/4858))
- Add `--cache_selected_only` flag to cache schema object of selected models only. ([#4688](https://github.com/dbt-labs/dbt-core/issues/4688), [#4860](https://github.com/dbt-labs/dbt-core/pull/4860))
- Support custom names for generic tests ([#3348](https://github.com/dbt-labs/dbt-core/issues/3348), [#4898](https://github.com/dbt-labs/dbt-core/pull/4898))
- Added Support for Semantic Versioning ([#4453](https://github.com/dbt-labs/dbt-core/issues/4453), [#4644](https://github.com/dbt-labs/dbt-core/pull/4644))
- New Dockerfile to support specific db adapters and platforms. See docker/README.md for details ([#4495](https://github.com/dbt-labs/dbt-core/issues/4495), [#4487](https://github.com/dbt-labs/dbt-core/pull/4487))
- Allow unique_key to take a list ([#2479](https://github.com/dbt-labs/dbt-core/issues/2479), [#4618](https://github.com/dbt-labs/dbt-core/pull/4618))
- Add `--quiet` global flag and `print` Jinja function ([#3451](https://github.com/dbt-labs/dbt-core/issues/3451), [#4701](https://github.com/dbt-labs/dbt-core/pull/4701))
- Add space before justification periods ([#4737](https://github.com/dbt-labs/dbt-core/issues/4737), [#4744](https://github.com/dbt-labs/dbt-core/pull/4744))
- Enable dbt jobs to run downstream models based on fresher sources. Compare the source freshness results between previous and current state. If any source is fresher and/or new in current vs. previous state, dbt will run and test the downstream models in scope. Example command: `dbt build --select source_status:fresher+` ([#4050](https://github.com/dbt-labs/dbt-core/issues/4050), [#4256](https://github.com/dbt-labs/dbt-core/pull/4256))
- converting unique key as list tests to new pytest format ([#4882](https://github.com/dbt-labs/dbt-core/issues/4882), [#4958](https://github.com/dbt-labs/dbt-core/pull/4958))
- Add a variable called selected_resources in the Jinja context containing a list of all the resources matching the nodes for the --select, --exclude and/or --selector parameters. ([#3471](https://github.com/dbt-labs/dbt-core/issues/3471), [#5001](https://github.com/dbt-labs/dbt-core/pull/5001))
- Support the DO_NOT_TRACK environment variable from the consoledonottrack.com initiative ([#3540](https://github.com/dbt-labs/dbt-core/issues/3540), [#5000](https://github.com/dbt-labs/dbt-core/pull/5000))
- Add `--no-print` global flag ([#4710](https://github.com/dbt-labs/dbt-core/issues/4710), [#4854](https://github.com/dbt-labs/dbt-core/pull/4854))
- add enabled as a source config ([#3662](https://github.com/dbt-labs/dbt-core/issues/3662), [#5008](https://github.com/dbt-labs/dbt-core/pull/5008))
### Fixes
- Fix bug causing empty node level meta, snapshot config errors ([#4459](https://github.com/dbt-labs/dbt-core/issues/4459), [#4726](https://github.com/dbt-labs/dbt-core/pull/4726))
- Inconsistent timestamps between inserted/updated and deleted rows in snapshots ([#4347](https://github.com/dbt-labs/dbt-core/issues/4347), [#4513](https://github.com/dbt-labs/dbt-core/pull/4513))
- Catch all Requests Exceptions on deps install to attempt retries. Also log the exceptions hit. ([#4849](https://github.com/dbt-labs/dbt-core/issues/4849), [#4865](https://github.com/dbt-labs/dbt-core/pull/4865))
- Use cli_vars instead of context to create package and selector renderers ([#4876](https://github.com/dbt-labs/dbt-core/issues/4876), [#4878](https://github.com/dbt-labs/dbt-core/pull/4878))
- depend on new dbt-extractor version with fixed github links ([#4891](https://github.com/dbt-labs/dbt-core/issues/4891), [#4890](https://github.com/dbt-labs/dbt-core/pull/4890))
- Update bumpervsion config to stop looking for missing setup.py ([#-1](https://github.com/dbt-labs/dbt-core/issues/-1), [#4896](https://github.com/dbt-labs/dbt-core/pull/4896))
- Corrected permissions settings for docker release workflow ([#4902](https://github.com/dbt-labs/dbt-core/issues/4902), [#4903](https://github.com/dbt-labs/dbt-core/pull/4903))
- User wasn't asked for permission to overwite a profile entry when running init inside an existing project ([#4375](https://github.com/dbt-labs/dbt-core/issues/4375), [#4447](https://github.com/dbt-labs/dbt-core/pull/4447))
- Add project name validation to `dbt init` ([#4490](https://github.com/dbt-labs/dbt-core/issues/4490), [#4536](https://github.com/dbt-labs/dbt-core/pull/4536))
- Allow override of string and numeric types for adapters. ([#4603](https://github.com/dbt-labs/dbt-core/issues/4603), [#4604](https://github.com/dbt-labs/dbt-core/pull/4604))
- A change in secret environment variables won't trigger a full reparse ([#4650](https://github.com/dbt-labs/dbt-core/issues/4650), [#4665](https://github.com/dbt-labs/dbt-core/pull/4665))
- Fix misspellings and typos in docstrings ([#4904](https://github.com/dbt-labs/dbt-core/issues/4904), [#4545](https://github.com/dbt-labs/dbt-core/pull/4545))
- Catch more cases to retry package retrieval for deps pointing to the hub. Also start to cache the package requests. ([#4849](https://github.com/dbt-labs/dbt-core/issues/4849), [#4982](https://github.com/dbt-labs/dbt-core/pull/4982))
- Make the warning message for a full event deque more descriptive ([#4962](https://github.com/dbt-labs/dbt-core/issues/4962), [#5011](https://github.com/dbt-labs/dbt-core/pull/5011))
- Fix hard delete snapshot test ([#4916](https://github.com/dbt-labs/dbt-core/issues/4916), [#5020](https://github.com/dbt-labs/dbt-core/pull/5020))
- Restore ability to utilize `updated_at` for check_cols snapshots ([#5076](https://github.com/dbt-labs/dbt-core/issues/5076), [#5077](https://github.com/dbt-labs/dbt-core/pull/5077))
- Use yaml renderer (with target context) for rendering selectors ([#5131](https://github.com/dbt-labs/dbt-core/issues/5131), [#5136](https://github.com/dbt-labs/dbt-core/pull/5136))
- Fix retry logic to return values after initial try ([#5023](https://github.com/dbt-labs/dbt-core/issues/5023), [#5137](https://github.com/dbt-labs/dbt-core/pull/5137))
- Scrub secret env vars from CommandError in exception stacktrace ([#5151](https://github.com/dbt-labs/dbt-core/issues/5151), [#5152](https://github.com/dbt-labs/dbt-core/pull/5152))
### Docs
- Resolve errors related to operations preventing DAG from generating in the docs. Also patch a spark issue to allow search to filter accurately past the missing columns. ([#4578](https://github.com/dbt-labs/dbt-core/issues/4578), [#4763](https://github.com/dbt-labs/dbt-core/pull/4763))
- Fixed capitalization in UI for exposures of `type: ml` ([#4984](https://github.com/dbt-labs/dbt-core/issues/4984), [#4995](https://github.com/dbt-labs/dbt-core/pull/4995))
- List packages and tags in alphabetical order ([#4984](https://github.com/dbt-labs/dbt-core/issues/4984), [#4995](https://github.com/dbt-labs/dbt-core/pull/4995))
- Bump jekyll from 3.8.7 to 3.9.0 ([#4984](https://github.com/dbt-labs/dbt-core/issues/4984), [#4995](https://github.com/dbt-labs/dbt-core/pull/4995))
- Updated docker README to reflect necessity of using BuildKit ([#4990](https://github.com/dbt-labs/dbt-core/issues/4990), [#5018](https://github.com/dbt-labs/dbt-core/pull/5018))
### Under the Hood
- Automate changelog generation with changie ([#4652](https://github.com/dbt-labs/dbt-core/issues/4652), [#4743](https://github.com/dbt-labs/dbt-core/pull/4743))
- add performance regression testing runner without orchestration ([#4021](https://github.com/dbt-labs/dbt-core/issues/4021), [#4602](https://github.com/dbt-labs/dbt-core/pull/4602))
- Fix broken links for changelog generation and tweak GHA to only post a comment once when changelog entry is missing. ([#4848](https://github.com/dbt-labs/dbt-core/issues/4848), [#4857](https://github.com/dbt-labs/dbt-core/pull/4857))
- Add support for Python 3.10 ([#4562](https://github.com/dbt-labs/dbt-core/issues/4562), [#4866](https://github.com/dbt-labs/dbt-core/pull/4866))
- Enable more dialects to snapshot sources with added columns, even those that don't support boolean datatypes ([#4488](https://github.com/dbt-labs/dbt-core/issues/4488), [#4871](https://github.com/dbt-labs/dbt-core/pull/4871))
- Add Graph Compilation and Adapter Cache tracking ([#4625](https://github.com/dbt-labs/dbt-core/issues/4625), [#4912](https://github.com/dbt-labs/dbt-core/pull/4912))
- Testing cleanup ([#3648](https://github.com/dbt-labs/dbt-core/issues/3648), [#4509](https://github.com/dbt-labs/dbt-core/pull/4509))
- Clean up test deprecation warnings ([#3988](https://github.com/dbt-labs/dbt-core/issues/3988), [#4556](https://github.com/dbt-labs/dbt-core/pull/4556))
- Use mashumaro for serialization in event logging ([#4504](https://github.com/dbt-labs/dbt-core/issues/4504), [#4505](https://github.com/dbt-labs/dbt-core/pull/4505))
- Drop support for Python 3.7.0 + 3.7.1 ([#4584](https://github.com/dbt-labs/dbt-core/issues/4584), [#4585](https://github.com/dbt-labs/dbt-core/pull/4585))
- Drop support for Python <3.7.2 ([#4584](https://github.com/dbt-labs/dbt-core/issues/4584), [#4643](https://github.com/dbt-labs/dbt-core/pull/4643))
- Re-format codebase (except tests) using pre-commit hooks ([#3195](https://github.com/dbt-labs/dbt-core/issues/3195), [#4697](https://github.com/dbt-labs/dbt-core/pull/4697))
- Add deps module README ([#4904](https://github.com/dbt-labs/dbt-core/issues/4904), [#4686](https://github.com/dbt-labs/dbt-core/pull/4686))
- Initial conversion of tests to pytest ([#4690](https://github.com/dbt-labs/dbt-core/issues/4690), [#4691](https://github.com/dbt-labs/dbt-core/pull/4691))
- Fix errors in Windows for tests/functions ([#4782](https://github.com/dbt-labs/dbt-core/issues/4782), [#4767](https://github.com/dbt-labs/dbt-core/pull/4767))
- Create a dbt.tests.adapter release when releasing dbt and postgres ([#4812](https://github.com/dbt-labs/dbt-core/issues/4812), [#4948](https://github.com/dbt-labs/dbt-core/pull/4948))
- update docker image to use python 3.10.3 ([#4904](https://github.com/dbt-labs/dbt-core/issues/4904), [#4963](https://github.com/dbt-labs/dbt-core/pull/4963))
- updates black to 22.3.0 which fixes dependency incompatibility when running with precommit. ([#4904](https://github.com/dbt-labs/dbt-core/issues/4904), [#4972](https://github.com/dbt-labs/dbt-core/pull/4972))
- Adds config util for ad-hoc creation of project objs or dicts ([#4808](https://github.com/dbt-labs/dbt-core/issues/4808), [#4981](https://github.com/dbt-labs/dbt-core/pull/4981))
- Remove TableComparison and convert existing calls to use dbt.tests.util ([#4778](https://github.com/dbt-labs/dbt-core/issues/4778), [#4986](https://github.com/dbt-labs/dbt-core/pull/4986))
- Remove unneeded create_schema in snapshot materialization ([#4742](https://github.com/dbt-labs/dbt-core/issues/4742), [#4993](https://github.com/dbt-labs/dbt-core/pull/4993))
- Added .git-blame-ignore-revs file to mask re-formmating commits from git blame ([#5004](https://github.com/dbt-labs/dbt-core/issues/5004), [#5019](https://github.com/dbt-labs/dbt-core/pull/5019))
- Convert version tests to pytest ([#5024](https://github.com/dbt-labs/dbt-core/issues/5024), [#5026](https://github.com/dbt-labs/dbt-core/pull/5026))
- Updating tests and docs to show that we now support Python 3.10 ([#4974](https://github.com/dbt-labs/dbt-core/issues/4974), [#5025](https://github.com/dbt-labs/dbt-core/pull/5025))
- Update --version output and logic ([#4724](https://github.com/dbt-labs/dbt-core/issues/4724), [#5029](https://github.com/dbt-labs/dbt-core/pull/5029))
- ([#5033](https://github.com/dbt-labs/dbt-core/issues/5033), [#5032](https://github.com/dbt-labs/dbt-core/pull/5032))
### Contributors
- [@NiallRees](https://github.com/NiallRees) ([#4447](https://github.com/dbt-labs/dbt-core/pull/4447))
- [@agoblet](https://github.com/agoblet) ([#5000](https://github.com/dbt-labs/dbt-core/pull/5000))
- [@alswang18](https://github.com/alswang18) ([#4644](https://github.com/dbt-labs/dbt-core/pull/4644))
- [@amirkdv](https://github.com/amirkdv) ([#4536](https://github.com/dbt-labs/dbt-core/pull/4536))
- [@anaisvaillant](https://github.com/anaisvaillant) ([#4256](https://github.com/dbt-labs/dbt-core/pull/4256))
- [@b-per](https://github.com/b-per) ([#5001](https://github.com/dbt-labs/dbt-core/pull/5001))
- [@dbeatty10](https://github.com/dbeatty10) ([#5077](https://github.com/dbt-labs/dbt-core/pull/5077))
- [@ehmartens](https://github.com/ehmartens) ([#4701](https://github.com/dbt-labs/dbt-core/pull/4701))
- [@joellabes](https://github.com/joellabes) ([#4744](https://github.com/dbt-labs/dbt-core/pull/4744))
- [@jonstacks](https://github.com/jonstacks) ([#4995](https://github.com/dbt-labs/dbt-core/pull/4995))
- [@kadero](https://github.com/kadero) ([#4513](https://github.com/dbt-labs/dbt-core/pull/4513))
- [@karunpoudel](https://github.com/karunpoudel) ([#4860](https://github.com/dbt-labs/dbt-core/pull/4860), [#4860](https://github.com/dbt-labs/dbt-core/pull/4860))
- [@kazanzhy](https://github.com/kazanzhy) ([#4545](https://github.com/dbt-labs/dbt-core/pull/4545))
- [@matt-winkler](https://github.com/matt-winkler) ([#4256](https://github.com/dbt-labs/dbt-core/pull/4256))
- [@mdesmet](https://github.com/mdesmet) ([#4604](https://github.com/dbt-labs/dbt-core/pull/4604))
- [@pgoslatara](https://github.com/pgoslatara) ([#4995](https://github.com/dbt-labs/dbt-core/pull/4995))
- [@poloaraujo](https://github.com/poloaraujo) ([#4854](https://github.com/dbt-labs/dbt-core/pull/4854))
- [@sungchun12](https://github.com/sungchun12) ([#4256](https://github.com/dbt-labs/dbt-core/pull/4256))
- [@willbowditch](https://github.com/willbowditch) ([#4777](https://github.com/dbt-labs/dbt-core/pull/4777))
## Previous Releases ## Previous Releases
For information on prior major and minor releases, see their changelogs: For information on prior major and minor releases, see their changelogs:
* [1.4](https://github.com/dbt-labs/dbt-core/blob/1.4.latest/CHANGELOG.md)
* [1.3](https://github.com/dbt-labs/dbt-core/blob/1.3.latest/CHANGELOG.md)
* [1.2](https://github.com/dbt-labs/dbt-core/blob/1.2.latest/CHANGELOG.md)
* [1.1](https://github.com/dbt-labs/dbt-core/blob/1.1.latest/CHANGELOG.md)
* [1.0](https://github.com/dbt-labs/dbt-core/blob/1.0.latest/CHANGELOG.md) * [1.0](https://github.com/dbt-labs/dbt-core/blob/1.0.latest/CHANGELOG.md)
* [0.21](https://github.com/dbt-labs/dbt-core/blob/0.21.latest/CHANGELOG.md) * [0.21](https://github.com/dbt-labs/dbt-core/blob/0.21.latest/CHANGELOG.md)
* [0.20](https://github.com/dbt-labs/dbt-core/blob/0.20.latest/CHANGELOG.md) * [0.20](https://github.com/dbt-labs/dbt-core/blob/0.20.latest/CHANGELOG.md)

View File

@@ -1,30 +1,79 @@
# Contributing to `dbt-core` # Contributing to `dbt`
`dbt-core` is open source software. It is what it is today because community members have opened issues, provided feedback, and [contributed to the knowledge loop](https://www.getdbt.com/dbt-labs/values/). Whether you are a seasoned open source contributor or a first-time committer, we welcome and encourage you to contribute code, documentation, ideas, or problem statements to this project.
1. [About this document](#about-this-document) 1. [About this document](#about-this-document)
2. [Getting the code](#getting-the-code) 2. [Proposing a change](#proposing-a-change)
3. [Setting up an environment](#setting-up-an-environment) 3. [Getting the code](#getting-the-code)
4. [Running `dbt` in development](#running-dbt-core-in-development) 4. [Setting up an environment](#setting-up-an-environment)
5. [Testing dbt-core](#testing) 5. [Running `dbt` in development](#running-dbt-in-development)
6. [Debugging](#debugging) 6. [Testing](#testing)
7. [Adding a changelog entry](#adding-a-changelog-entry) 7. [Submitting a Pull Request](#submitting-a-pull-request)
8. [Submitting a Pull Request](#submitting-a-pull-request)
## About this document ## About this document
There are many ways to contribute to the ongoing development of `dbt-core`, such as by participating in discussions and issues. We encourage you to first read our higher-level document: ["Expectations for Open Source Contributors"](https://docs.getdbt.com/docs/contributing/oss-expectations). This document is a guide intended for folks interested in contributing to `dbt-core`. Below, we document the process by which members of the community should create issues and submit pull requests (PRs) in this repository. It is not intended as a guide for using `dbt-core`, and it assumes a certain level of familiarity with Python concepts such as virtualenvs, `pip`, python modules, filesystems, and so on. This guide assumes you are using macOS or Linux and are comfortable with the command line.
The rest of this document serves as a more granular guide for contributing code changes to `dbt-core` (this repository). It is not intended as a guide for using `dbt-core`, and some pieces assume a level of familiarity with Python development (virtualenvs, `pip`, etc). Specific code snippets in this guide assume you are using macOS or Linux and are comfortable with the command line. If you're new to python development or contributing to open-source software, we encourage you to read this document from start to finish. If you get stuck, drop us a line in the `#dbt-core-development` channel on [slack](https://community.getdbt.com).
If you get stuck, we're happy to help! Drop us a line in the `#dbt-core-development` channel in the [dbt Community Slack](https://community.getdbt.com). #### Adapters
### Notes If you have an issue or code change suggestion related to a specific database [adapter](https://docs.getdbt.com/docs/available-adapters); please refer to that supported databases seperate repo for those contributions.
- **Adapters:** Is your issue or proposed code change related to a specific [database adapter](https://docs.getdbt.com/docs/available-adapters)? If so, please open issues, PRs, and discussions in that adapter's repository instead. The sole exception is Postgres; the `dbt-postgres` plugin lives in this repository (`dbt-core`). ### Signing the CLA
- **CLA:** Please note that anyone contributing code to `dbt-core` must sign the [Contributor License Agreement](https://docs.getdbt.com/docs/contributor-license-agreements). If you are unable to sign the CLA, the `dbt-core` maintainers will unfortunately be unable to merge any of your Pull Requests. We welcome you to participate in discussions, open issues, and comment on existing ones.
- **Branches:** All pull requests from community contributors should target the `main` branch (default). If the change is needed as a patch for a minor version of dbt that has already been released (or is already a release candidate), a maintainer will backport the changes in your PR to the relevant "latest" release branch (`1.0.latest`, `1.1.latest`, ...). If an issue fix applies to a release branch, that fix should be first committed to the development branch and then to the release branch (rarely release-branch fixes may not apply to `main`). Please note that all contributors to `dbt-core` must sign the [Contributor License Agreement](https://docs.getdbt.com/docs/contributor-license-agreements) to have their Pull Request merged into the `dbt-core` codebase. If you are unable to sign the CLA, then the `dbt-core` maintainers will unfortunately be unable to merge your Pull Request. You are, however, welcome to open issues and comment on existing ones.
- **Releases**: Before releasing a new minor version of Core, we prepare a series of alphas and release candidates to allow users (especially employees of dbt Labs!) to test the new version in live environments. This is an important quality assurance step, as it exposes the new code to a wide variety of complicated deployments and can surface bugs before official release. Releases are accessible via pip, homebrew, and dbt Cloud.
## Proposing a change
`dbt-core` is Apache 2.0-licensed open source software. `dbt-core` is what it is today because community members like you have opened issues, provided feedback, and contributed to the knowledge loop for the entire communtiy. Whether you are a seasoned open source contributor or a first-time committer, we welcome and encourage you to contribute code, documentation, ideas, or problem statements to this project.
### Defining the problem
If you have an idea for a new feature or if you've discovered a bug in `dbt-core`, the first step is to open an issue. Please check the list of [open issues](https://github.com/dbt-labs/dbt-core/issues) before creating a new one. If you find a relevant issue, please add a comment to the open issue instead of creating a new one. There are hundreds of open issues in this repository and it can be hard to know where to look for a relevant open issue. **The `dbt-core` maintainers are always happy to point contributors in the right direction**, so please err on the side of documenting your idea in a new issue if you are unsure where a problem statement belongs.
> **Note:** All community-contributed Pull Requests _must_ be associated with an open issue. If you submit a Pull Request that does not pertain to an open issue, you will be asked to create an issue describing the problem before the Pull Request can be reviewed.
### Discussing the idea
After you open an issue, a `dbt-core` maintainer will follow up by commenting on your issue (usually within 1-3 days) to explore your idea further and advise on how to implement the suggested changes. In many cases, community members will chime in with their own thoughts on the problem statement. If you as the issue creator are interested in submitting a Pull Request to address the issue, you should indicate this in the body of the issue. The `dbt-core` maintainers are _always_ happy to help contributors with the implementation of fixes and features, so please also indicate if there's anything you're unsure about or could use guidance around in the issue.
### Submitting a change
If an issue is appropriately well scoped and describes a beneficial change to the `dbt-core` codebase, then anyone may submit a Pull Request to implement the functionality described in the issue. See the sections below on how to do this.
The `dbt-core` maintainers will add a `good first issue` label if an issue is suitable for a first-time contributor. This label often means that the required code change is small, limited to one database adapter, or a net-new addition that does not impact existing functionality. You can see the list of currently open issues on the [Contribute](https://github.com/dbt-labs/dbt-core/contribute) page.
Here's a good workflow:
- Comment on the open issue, expressing your interest in contributing the required code change
- Outline your planned implementation. If you want help getting started, ask!
- Follow the steps outlined below to develop locally. Once you have opened a PR, one of the `dbt-core` maintainers will work with you to review your code.
- Add a test! Tests are crucial for both fixes and new features alike. We want to make sure that code works as intended, and that it avoids any bugs previously encountered. Currently, the best resource for understanding `dbt-core`'s [unit](test/unit) and [integration](test/integration) tests is the tests themselves. One of the maintainers can help by pointing out relevant examples.
- Check your formatting and linting with [Flake8](https://flake8.pycqa.org/en/latest/#), [Black](https://github.com/psf/black), and the rest of the hooks we have in our [pre-commit](https://pre-commit.com/) [config](https://github.com/dbt-labs/dbt-core/blob/75201be9db1cb2c6c01fa7e71a314f5e5beb060a/.pre-commit-config.yaml).
In some cases, the right resolution to an open issue might be tangential to the `dbt-core` codebase. The right path forward might be a documentation update or a change that can be made in user-space. In other cases, the issue might describe functionality that the `dbt-core` maintainers are unwilling or unable to incorporate into the `dbt-core` codebase. When it is determined that an open issue describes functionality that will not translate to a code change in the `dbt-core` repository, the issue will be tagged with the `wontfix` label (see below) and closed.
### Using issue labels
The `dbt-core` maintainers use labels to categorize open issues. Most labels describe the domain in the `dbt-core` codebase germane to the discussion.
| tag | description |
| --- | ----------- |
| [triage](https://github.com/dbt-labs/dbt-core/labels/triage) | This is a new issue which has not yet been reviewed by a `dbt-core` maintainer. This label is removed when a maintainer reviews and responds to the issue. |
| [bug](https://github.com/dbt-labs/dbt-core/labels/bug) | This issue represents a defect or regression in `dbt-core` |
| [enhancement](https://github.com/dbt-labs/dbt-core/labels/enhancement) | This issue represents net-new functionality in `dbt-core` |
| [good first issue](https://github.com/dbt-labs/dbt-core/labels/good%20first%20issue) | This issue does not require deep knowledge of the `dbt-core` codebase to implement. This issue is appropriate for a first-time contributor. |
| [help wanted](https://github.com/dbt-labs/dbt-core/labels/help%20wanted) / [discussion](https://github.com/dbt-labs/dbt-core/labels/discussion) | Conversation around this issue in ongoing, and there isn't yet a clear path forward. Input from community members is most welcome. |
| [duplicate](https://github.com/dbt-labs/dbt-core/issues/duplicate) | This issue is functionally identical to another open issue. The `dbt-core` maintainers will close this issue and encourage community members to focus conversation on the other one. |
| [snoozed](https://github.com/dbt-labs/dbt-core/labels/snoozed) | This issue describes a good idea, but one which will probably not be addressed in a six-month time horizon. The `dbt-core` maintainers will revist these issues periodically and re-prioritize them accordingly. |
| [stale](https://github.com/dbt-labs/dbt-core/labels/stale) | This is an old issue which has not recently been updated. Stale issues will periodically be closed by `dbt-core` maintainers, but they can be re-opened if the discussion is restarted. |
| [wontfix](https://github.com/dbt-labs/dbt-core/labels/wontfix) | This issue does not require a code change in the `dbt-core` repository, or the maintainers are unwilling/unable to merge a Pull Request which implements the behavior described in the issue. |
#### Branching Strategy
`dbt-core` has three types of branches:
- **Trunks** are where active development of the next release takes place. There is one trunk named `main` at the time of writing this, and will be the default branch of the repository.
- **Release Branches** track a specific, not yet complete release of `dbt-core`. Each minor version release has a corresponding release branch. For example, the `0.11.x` series of releases has a branch called `0.11.latest`. This allows us to release new patch versions under `0.11` without necessarily needing to pull them into the latest version of `dbt-core`.
- **Feature Branches** track individual features and fixes. On completion they should be merged into the trunk branch or a specific release branch.
## Getting the code ## Getting the code
@@ -36,17 +85,15 @@ You will need `git` in order to download and modify the `dbt-core` source code.
If you are not a member of the `dbt-labs` GitHub organization, you can contribute to `dbt-core` by forking the `dbt-core` repository. For a detailed overview on forking, check out the [GitHub docs on forking](https://help.github.com/en/articles/fork-a-repo). In short, you will need to: If you are not a member of the `dbt-labs` GitHub organization, you can contribute to `dbt-core` by forking the `dbt-core` repository. For a detailed overview on forking, check out the [GitHub docs on forking](https://help.github.com/en/articles/fork-a-repo). In short, you will need to:
1. Fork the `dbt-core` repository 1. fork the `dbt-core` repository
2. Clone your fork locally 2. clone your fork locally
3. Check out a new branch for your proposed changes 3. check out a new branch for your proposed changes
4. Push changes to your fork 4. push changes to your fork
5. Open a pull request against `dbt-labs/dbt-core` from your forked repository 5. open a pull request against `dbt-labs/dbt` from your forked repository
### dbt Labs contributors ### dbt Labs contributors
If you are a member of the `dbt-labs` GitHub organization, you will have push access to the `dbt-core` repo. Rather than forking `dbt-core` to make your changes, just clone the repository, check out a new branch, and push directly to that branch. Branch names should be fixed by `CT-XXX/` where: If you are a member of the `dbt-labs` GitHub organization, you will have push access to the `dbt-core` repo. Rather than forking `dbt-core` to make your changes, just clone the repository, check out a new branch, and push directly to that branch.
* CT stands for 'core team'
* XXX stands for a JIRA ticket number
## Setting up an environment ## Setting up an environment
@@ -54,21 +101,19 @@ There are some tools that will be helpful to you in developing locally. While th
### Tools ### Tools
These are the tools used in `dbt-core` development and testing: A short list of tools used in `dbt-core` testing that will be helpful to your understanding:
- [`tox`](https://tox.readthedocs.io/en/latest/) to manage virtualenvs across python versions. We currently target the latest patch releases for Python 3.7, 3.8, 3.9, 3.10 and 3.11 - [`tox`](https://tox.readthedocs.io/en/latest/) to manage virtualenvs across python versions. We currently target the latest patch releases for Python 3.7, Python 3.8, Python 3.9, and Python 3.10
- [`pytest`](https://docs.pytest.org/en/latest/) to define, discover, and run tests - [`pytest`](https://docs.pytest.org/en/latest/) to discover/run tests
- [`make`](https://users.cs.duke.edu/~ola/courses/programming/Makefiles/Makefiles.html) - but don't worry too much, nobody _really_ understands how make works and our Makefile is super simple
- [`flake8`](https://flake8.pycqa.org/en/latest/) for code linting - [`flake8`](https://flake8.pycqa.org/en/latest/) for code linting
- [`black`](https://github.com/psf/black) for code formatting - [`black`](https://github.com/psf/black) for code formatting
- [`mypy`](https://mypy.readthedocs.io/en/stable/) for static type checking - [`mypy`](https://mypy.readthedocs.io/en/stable/) for static type checking
- [`pre-commit`](https://pre-commit.com) to easily run those checks - [Github Actions](https://github.com/features/actions)
- [`changie`](https://changie.dev/) to create changelog entries, without merge conflicts
- [`make`](https://users.cs.duke.edu/~ola/courses/programming/Makefiles/Makefiles.html) to run multiple setup or test steps in combination. Don't worry too much, nobody _really_ understands how `make` works, and our Makefile aims to be super simple.
- [GitHub Actions](https://github.com/features/actions) for automating tests and checks, once a PR is pushed to the `dbt-core` repository
A deep understanding of these tools in not required to effectively contribute to `dbt-core`, but we recommend checking out the attached documentation if you're interested in learning more about each one. A deep understanding of these tools in not required to effectively contribute to `dbt-core`, but we recommend checking out the attached documentation if you're interested in learning more about them.
#### Virtual environments #### virtual environments
We strongly recommend using virtual environments when developing code in `dbt-core`. We recommend creating this virtualenv We strongly recommend using virtual environments when developing code in `dbt-core`. We recommend creating this virtualenv
in the root of the `dbt-core` repository. To create a new virtualenv, run: in the root of the `dbt-core` repository. To create a new virtualenv, run:
@@ -79,12 +124,12 @@ source env/bin/activate
This will create and activate a new Python virtual environment. This will create and activate a new Python virtual environment.
#### Docker and `docker-compose` #### docker and docker-compose
Docker and `docker-compose` are both used in testing. Specific instructions for you OS can be found [here](https://docs.docker.com/get-docker/). Docker and docker-compose are both used in testing. Specific instructions for you OS can be found [here](https://docs.docker.com/get-docker/).
#### Postgres (optional) #### postgres (optional)
For testing, and later in the examples in this document, you may want to have `psql` available so you can poke around in the database and see what happened. We recommend that you use [homebrew](https://brew.sh/) for that on macOS, and your package manager on Linux. You can install any version of the postgres client that you'd like. On macOS, with homebrew setup, you can run: For testing, and later in the examples in this document, you may want to have `psql` available so you can poke around in the database and see what happened. We recommend that you use [homebrew](https://brew.sh/) for that on macOS, and your package manager on Linux. You can install any version of the postgres client that you'd like. On macOS, with homebrew setup, you can run:
@@ -96,37 +141,32 @@ brew install postgresql
### Installation ### Installation
First make sure that you set up your `virtualenv` as described in [Setting up an environment](#setting-up-an-environment). Also ensure you have the latest version of pip installed with `pip install --upgrade pip`. Next, install `dbt-core` (and its dependencies): First make sure that you set up your `virtualenv` as described in [Setting up an environment](#setting-up-an-environment). Also ensure you have the latest version of pip installed with `pip install --upgrade pip`. Next, install `dbt-core` (and its dependencies) with:
```sh ```sh
make dev make dev
``` # or
or, alternatively:
```sh
pip install -r dev-requirements.txt -r editable-requirements.txt pip install -r dev-requirements.txt -r editable-requirements.txt
pre-commit install
``` ```
When installed in this way, any changes you make to your local copy of the source code will be reflected immediately in your next `dbt` run. When `dbt-core` is installed this way, any changes you make to the `dbt-core` source code will be reflected immediately in your next `dbt-core` run.
### Running `dbt-core` ### Running `dbt-core`
With your virtualenv activated, the `dbt` script should point back to the source code you've cloned on your machine. You can verify this by running `which dbt`. This command should show you a path to an executable in your virtualenv. With your virtualenv activated, the `dbt-core` script should point back to the source code you've cloned on your machine. You can verify this by running `which dbt`. This command should show you a path to an executable in your virtualenv.
Configure your [profile](https://docs.getdbt.com/docs/configure-your-profile) as necessary to connect to your target databases. It may be a good idea to add a new profile pointing to a local Postgres instance, or a specific test sandbox within your data warehouse if appropriate. Configure your [profile](https://docs.getdbt.com/docs/configure-your-profile) as necessary to connect to your target databases. It may be a good idea to add a new profile pointing to a local postgres instance, or a specific test sandbox within your data warehouse if appropriate.
## Testing ## Testing
Once you're able to manually test that your code change is working as expected, it's important to run existing automated tests, as well as adding some new ones. These tests will ensure that: Getting the `dbt-core` integration tests set up in your local environment will be very helpful as you start to make changes to your local version of `dbt-core`. The section that follows outlines some helpful tips for setting up the test environment.
- Your code changes do not unexpectedly break other established functionality
- Your code changes can handle all known edge cases
- The functionality you're adding will _keep_ working in the future
Although `dbt-core` works with a number of different databases, you won't need to supply credentials for every one of these databases in your test environment. Instead, you can test most `dbt-core` code changes with Python and Postgres. Although `dbt-core` works with a number of different databases, you won't need to supply credentials for every one of these databases in your test environment. Instead you can test all dbt-core code changes with Python and Postgres.
### Initial setup ### Initial setup
Postgres offers the easiest way to test most `dbt-core` functionality today. They are the fastest to run, and the easiest to set up. To run the Postgres integration tests, you'll have to do one extra step of setting up the test database: We recommend starting with `dbt-core`'s Postgres tests. These tests cover most of the functionality in `dbt-core`, are the fastest to run, and are the easiest to set up. To run the Postgres integration tests, you'll have to do one extra step of setting up the test database:
```sh ```sh
make setup-db make setup-db
@@ -152,82 +192,48 @@ make test
# Runs postgres integration tests with py38 in "fail fast" mode. # Runs postgres integration tests with py38 in "fail fast" mode.
make integration make integration
``` ```
> These make targets assume you have a local installation of a recent version of [`tox`](https://tox.readthedocs.io/en/latest/) for unit/integration testing and pre-commit for code quality checks, > These make targets assume you have a local install of a recent version of [`tox`](https://tox.readthedocs.io/en/latest/) for unit/integration testing and pre-commit for code quality checks,
> unless you use choose a Docker container to run tests. Run `make help` for more info. > unless you use choose a Docker container to run tests. Run `make help` for more info.
Check out the other targets in the Makefile to see other commonly used test Check out the other targets in the Makefile to see other commonly used test
suites. suites.
#### `pre-commit` #### `pre-commit`
[`pre-commit`](https://pre-commit.com) takes care of running all code-checks for formatting and linting. Run `make dev` to install `pre-commit` in your local environment (we recommend running this command with a python virtual environment active). This command installs several pip executables including black, mypy, and flake8. Once this is done you can use any of the linter-based make targets as well as a git pre-commit hook that will ensure proper formatting and linting. [`pre-commit`](https.pre-commit.com) takes care of running all code-checks for formatting and linting. Run `make dev` to install `pre-commit` in your local environment. Once this is done you can use any of the linter-based make targets as well as a git pre-commit hook that will ensure proper formatting and linting.
#### `tox` #### `tox`
[`tox`](https://tox.readthedocs.io/en/latest/) takes care of managing virtualenvs and install dependencies in order to run tests. You can also run tests in parallel, for example, you can run unit tests for Python 3.7, Python 3.8, Python 3.9, Python 3.10 and Python 3.11 checks in parallel with `tox -p`. Also, you can run unit tests for specific python versions with `tox -e py37`. The configuration for these tests in located in `tox.ini`. [`tox`](https://tox.readthedocs.io/en/latest/) takes care of managing virtualenvs and install dependencies in order to run tests. You can also run tests in parallel, for example, you can run unit tests for Python 3.7, Python 3.8, Python 3.9, and Python 3.10 checks in parallel with `tox -p`. Also, you can run unit tests for specific python versions with `tox -e py37`. The configuration for these tests in located in `tox.ini`.
#### `pytest` #### `pytest`
Finally, you can also run a specific test or group of tests using [`pytest`](https://docs.pytest.org/en/latest/) directly. With a virtualenv active and dev dependencies installed you can do things like: Finally, you can also run a specific test or group of tests using [`pytest`](https://docs.pytest.org/en/latest/) directly. With a virtualenv
active and dev dependencies installed you can do things like:
```sh ```sh
# run specific postgres integration tests
python -m pytest -m profile_postgres test/integration/001_simple_copy_test
# run all unit tests in a file # run all unit tests in a file
python3 -m pytest test/unit/test_graph.py python -m pytest test/unit/test_graph.py
# run a specific unit test # run a specific unit test
python3 -m pytest test/unit/test_graph.py::GraphTest::test__dependency_list python -m pytest test/unit/test_graph.py::GraphTest::test__dependency_list
# run specific Postgres integration tests (old way)
python3 -m pytest -m profile_postgres test/integration/074_postgres_unlogged_table_tests
# run specific Postgres integration tests (new way)
python3 -m pytest tests/functional/sources
``` ```
> [Here](https://docs.pytest.org/en/reorganize-docs/new-docs/user/commandlineuseful.html)
> is a list of useful command-line options for `pytest` to use while developing.
> See [pytest usage docs](https://docs.pytest.org/en/6.2.x/usage.html) for an overview of useful command-line options. ## Adding CHANGELOG Entry
### Unit, Integration, Functional? We use [changie](https://changie.dev) to generate `CHANGELOG` entries. Do not edit the `CHANGELOG.md` directly. Your modifications will be lost.
Here are some general rules for adding tests:
* unit tests (`test/unit` & `tests/unit`) dont need to access a database; "pure Python" tests should be written as unit tests
* functional tests (`test/integration` & `tests/functional`) cover anything that interacts with a database, namely adapter
* *everything in* `test/*` *is being steadily migrated to* `tests/*`
## Debugging
1. The logs for a `dbt run` have stack traces and other information for debugging errors (in `logs/dbt.log` in your project directory).
2. Try using a debugger, like `ipdb`. For pytest: `--pdb --pdbcls=IPython.terminal.debugger:pdb`
3. Sometimes, its easier to debug on a single thread: `dbt --single-threaded run`
4. To make print statements from Jinja macros: `{{ log(msg, info=true) }}`
5. You can also add `{{ debug() }}` statements, which will drop you into some auto-generated code that the macro wrote.
6. The dbt “artifacts” are written out to the target directory of your dbt project. They are in unformatted json, which can be hard to read. Format them with:
> python -m json.tool target/run_results.json > run_results.json
### Assorted development tips
* Append `# type: ignore` to the end of a line if you need to disable `mypy` on that line.
* Sometimes flake8 complains about lines that are actually fine, in which case you can put a comment on the line such as: # noqa or # noqa: ANNN, where ANNN is the error code that flake8 issues.
* To collect output for `CProfile`, run dbt with the `-r` option and the name of an output file, i.e. `dbt -r dbt.cprof run`. If you just want to profile parsing, you can do: `dbt -r dbt.cprof parse`. `pip` install `snakeviz` to view the output. Run `snakeviz dbt.cprof` and output will be rendered in a browser window.
## Adding or modifying a CHANGELOG Entry
We use [changie](https://changie.dev) to generate `CHANGELOG` entries. **Note:** Do not edit the `CHANGELOG.md` directly. Your modifications will be lost.
Follow the steps to [install `changie`](https://changie.dev/guide/installation/) for your system. Follow the steps to [install `changie`](https://changie.dev/guide/installation/) for your system.
Once changie is installed and your PR is created for a new feature, simply run the following command and changie will walk you through the process of creating a changelog entry: Once changie is installed and your PR is created, simply run `changie new` and changie will walk you through the process of creating a changelog entry. Commit the file that's created and your changelog entry is complete!
```shell
changie new
```
Commit the file that's created and your changelog entry is complete!
If you are contributing to a feature already in progress, you will modify the changie yaml file in dbt/.changes/unreleased/ related to your change. If you need help finding this file, please ask within the discussion for the pull request!
You don't need to worry about which `dbt-core` version your change will go into. Just create the changelog entry with `changie`, and open your PR against the `main` branch. All merged changes will be included in the next minor version of `dbt-core`. The Core maintainers _may_ choose to "backport" specific changes in order to patch older minor versions. In that case, a maintainer will take care of that backport after merging your PR, before releasing the new version of `dbt-core`.
## Submitting a Pull Request ## Submitting a Pull Request
Code can be merged into the current development branch `main` by opening a pull request. A `dbt-core` maintainer will review your PR. They may suggest code revision for style or clarity, or request that you add unit or integration test(s). These are good things! We believe that, with a little bit of help, anyone can contribute high-quality code. dbt Labs provides a CI environment to test changes to specific adapters, and periodic maintenance checks of `dbt-core` through Github Actions. For example, if you submit a pull request to the `dbt-redshift` repo, GitHub will trigger automated code checks and tests against Redshift.
A `dbt-core` maintainer will review your PR. They may suggest code revision for style or clarity, or request that you add unit or integration test(s). These are good things! We believe that, with a little bit of help, anyone can contribute high-quality code.
- First time contributors should note code checks + unit tests require a maintainer to approve.
Automated tests run via GitHub Actions. If you're a first-time contributor, all tests (including code checks and unit tests) will require a maintainer to approve. Changes in the `dbt-core` repository trigger integration tests against Postgres. dbt Labs also provides CI environments in which to test changes to other adapters, triggered by PRs in those adapters' repositories, as well as periodic maintenance checks of each adapter in concert with the latest `dbt-core` code changes.
Once all tests are passing and your PR has been approved, a `dbt-core` maintainer will merge your changes into the active development branch. And that's it! Happy developing :tada: Once all tests are passing and your PR has been approved, a `dbt-core` maintainer will merge your changes into the active development branch. And that's it! Happy developing :tada:
Sometimes, the content license agreement auto-check bot doesn't find a user's entry in its roster. If you need to force a rerun, add `@cla-bot check` in a comment on the pull request.

View File

@@ -3,7 +3,7 @@
# See `/docker` for a generic and production-ready docker file # See `/docker` for a generic and production-ready docker file
## ##
FROM ubuntu:22.04 FROM ubuntu:20.04
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
@@ -49,9 +49,6 @@ RUN apt-get update \
python3.10 \ python3.10 \
python3.10-dev \ python3.10-dev \
python3.10-venv \ python3.10-venv \
python3.11 \
python3.11-dev \
python3.11-venv \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

View File

@@ -6,27 +6,10 @@ ifeq ($(USE_DOCKER),true)
DOCKER_CMD := docker-compose run --rm test DOCKER_CMD := docker-compose run --rm test
endif endif
LOGS_DIR := ./logs
# Optional flag to invoke tests using our CI env.
# But we always want these active for structured
# log testing.
CI_FLAGS =\
DBT_TEST_USER_1=dbt_test_user_1\
DBT_TEST_USER_2=dbt_test_user_2\
DBT_TEST_USER_3=dbt_test_user_3\
RUSTFLAGS="-D warnings"\
LOG_DIR=./logs\
DBT_LOG_FORMAT=json
.PHONY: dev_req
dev_req: ## Installs dbt-* packages in develop mode along with only development dependencies.
@\
pip install -r dev-requirements.txt -r editable-requirements.txt
.PHONY: dev .PHONY: dev
dev: dev_req ## Installs dbt-* packages in develop mode along with development dependencies and pre-commit. dev: ## Installs dbt-* packages in develop mode along with development dependencies.
@\ @\
pip install -r dev-requirements.txt -r editable-requirements.txt && \
pre-commit install pre-commit install
.PHONY: mypy .PHONY: mypy
@@ -51,34 +34,27 @@ lint: .env ## Runs flake8 and mypy code checks against staged changes.
$(DOCKER_CMD) pre-commit run mypy-check --hook-stage manual | grep -v "INFO" $(DOCKER_CMD) pre-commit run mypy-check --hook-stage manual | grep -v "INFO"
.PHONY: unit .PHONY: unit
unit: .env ## Runs unit tests with py unit: .env ## Runs unit tests with py38.
@\ @\
$(DOCKER_CMD) tox -e py $(DOCKER_CMD) tox -e py38
.PHONY: test .PHONY: test
test: .env ## Runs unit tests with py and code checks against staged changes. test: .env ## Runs unit tests with py38 and code checks against staged changes.
@\ @\
$(DOCKER_CMD) tox -e py; \ $(DOCKER_CMD) tox -e py38; \
$(DOCKER_CMD) pre-commit run black-check --hook-stage manual | grep -v "INFO"; \ $(DOCKER_CMD) pre-commit run black-check --hook-stage manual | grep -v "INFO"; \
$(DOCKER_CMD) pre-commit run flake8-check --hook-stage manual | grep -v "INFO"; \ $(DOCKER_CMD) pre-commit run flake8-check --hook-stage manual | grep -v "INFO"; \
$(DOCKER_CMD) pre-commit run mypy-check --hook-stage manual | grep -v "INFO" $(DOCKER_CMD) pre-commit run mypy-check --hook-stage manual | grep -v "INFO"
.PHONY: integration .PHONY: integration
integration: .env ## Runs postgres integration tests with py-integration integration: .env ## Runs postgres integration tests with py38.
@\ @\
$(if $(USE_CI_FLAGS), $(CI_FLAGS)) $(DOCKER_CMD) tox -e py-integration -- -nauto $(DOCKER_CMD) tox -e py38-integration -- -nauto
.PHONY: integration-fail-fast .PHONY: integration-fail-fast
integration-fail-fast: .env ## Runs postgres integration tests with py-integration in "fail fast" mode. integration-fail-fast: .env ## Runs postgres integration tests with py38 in "fail fast" mode.
@\ @\
$(DOCKER_CMD) tox -e py-integration -- -x -nauto $(DOCKER_CMD) tox -e py38-integration -- -x -nauto
.PHONY: interop
interop: clean
@\
mkdir $(LOGS_DIR) && \
$(CI_FLAGS) $(DOCKER_CMD) tox -e py-integration -- -nauto && \
LOG_DIR=$(LOGS_DIR) cargo run --manifest-path test/interop/log_parsing/Cargo.toml
.PHONY: setup-db .PHONY: setup-db
setup-db: ## Setup Postgres database with docker-compose for system testing. setup-db: ## Setup Postgres database with docker-compose for system testing.
@@ -101,7 +77,6 @@ endif
clean: ## Resets development environment. clean: ## Resets development environment.
@echo 'cleaning repo...' @echo 'cleaning repo...'
@rm -f .coverage @rm -f .coverage
@rm -f .coverage.*
@rm -rf .eggs/ @rm -rf .eggs/
@rm -f .env @rm -f .env
@rm -rf .tox/ @rm -rf .tox/

View File

@@ -9,7 +9,7 @@
**[dbt](https://www.getdbt.com/)** enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications. **[dbt](https://www.getdbt.com/)** enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications.
![architecture](https://github.com/dbt-labs/dbt-core/blob/202cb7e51e218c7b29eb3b11ad058bd56b7739de/etc/dbt-transform.png) ![architecture](https://raw.githubusercontent.com/dbt-labs/dbt-core/6c6649f9129d5d108aa3b0526f634cd8f3a9d1ed/etc/dbt-arch.png)
## Understanding dbt ## Understanding dbt

View File

@@ -1,2 +1 @@
recursive-include dbt/include *.py *.sql *.yml *.html *.md .gitkeep .gitignore recursive-include dbt/include *.py *.sql *.yml *.html *.md .gitkeep .gitignore
include dbt/py.typed

View File

@@ -2,59 +2,50 @@
## The following are individual files in this directory. ## The following are individual files in this directory.
### compilation.py
### constants.py
### dataclass_schema.py
### deprecations.py ### deprecations.py
### exceptions.py
### flags.py ### flags.py
### helper_types.py
### hooks.py
### lib.py
### links.py
### logger.py
### main.py ### main.py
### node_types.py
### profiler.py
### selected_resources.py
### semver.py
### tracking.py ### tracking.py
### version.py
### lib.py
### node_types.py
### helper_types.py
### links.py
### semver.py
### ui.py ### ui.py
### compilation.py
### dataclass_schema.py
### exceptions.py
### hooks.py
### logger.py
### profiler.py
### utils.py ### utils.py
### version.py
## The subdirectories will be documented in a README in the subdirectory ## The subdirectories will be documented in a README in the subdirectory
* adapters
* cli
* clients
* config * config
* context
* contracts
* deps
* docs
* events
* graph
* include * include
* parser * adapters
* context
* deps
* graph
* task * task
* tests * clients
* events

View File

@@ -1,7 +0,0 @@
# N.B.
# This will add to the packages __path__ all subdirectories of directories on sys.path named after the package which effectively combines both modules into a single namespace (dbt.adapters)
# The matching statement is in plugins/postgres/dbt/__init__.py
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

View File

@@ -1,30 +1 @@
# Adapters README # Adapters README
The Adapters module is responsible for defining database connection methods, caching information from databases, how relations are defined, and the two major connection types we have - base and sql.
# Directories
## `base`
Defines the base implementation Adapters can use to build out full functionality.
## `sql`
Defines a sql implementation for adapters that initially inherits the above base implementation and comes with some premade methods and macros that can be overwritten as needed per adapter. (most common type of adapter.)
# Files
## `cache.py`
Cached information from the database.
## `factory.py`
Defines how we generate adapter objects
## `protocol.py`
Defines various interfaces for various adapter objects. Helps mypy correctly resolve methods.
## `reference_keys.py`
Configures naming scheme for cache elements to be universal.

View File

@@ -1,7 +0,0 @@
# N.B.
# This will add to the packages __path__ all subdirectories of directories on sys.path named after the package which effectively combines both modules into a single namespace (dbt.adapters)
# The matching statement is in plugins/postgres/dbt/adapters/__init__.py
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

View File

@@ -1,10 +0,0 @@
## Base adapters
### impl.py
The class `SQLAdapter` in [base/imply.py](https://github.com/dbt-labs/dbt-core/blob/main/core/dbt/adapters/base/impl.py) is a (mostly) abstract object that adapter objects inherit from. The base class scaffolds out methods that every adapter project usually should implement for smooth communication between dbt and database.
Some target databases require more or fewer methods--it all depends on what the warehouse's featureset is.
Look into the class for function-level comments.

View File

@@ -10,5 +10,5 @@ from dbt.adapters.base.relation import ( # noqa
SchemaSearchMap, SchemaSearchMap,
) )
from dbt.adapters.base.column import Column # noqa from dbt.adapters.base.column import Column # noqa
from dbt.adapters.base.impl import AdapterConfig, BaseAdapter, PythonJobHelper # noqa from dbt.adapters.base.impl import AdapterConfig, BaseAdapter # noqa
from dbt.adapters.base.plugin import AdapterPlugin # noqa from dbt.adapters.base.plugin import AdapterPlugin # noqa

View File

@@ -2,7 +2,7 @@ from dataclasses import dataclass
import re import re
from typing import Dict, ClassVar, Any, Optional from typing import Dict, ClassVar, Any, Optional
from dbt.exceptions import DbtRuntimeError from dbt.exceptions import RuntimeException
@dataclass @dataclass
@@ -12,7 +12,6 @@ class Column:
"TIMESTAMP": "TIMESTAMP", "TIMESTAMP": "TIMESTAMP",
"FLOAT": "FLOAT", "FLOAT": "FLOAT",
"INTEGER": "INT", "INTEGER": "INT",
"BOOLEAN": "BOOLEAN",
} }
column: str column: str
dtype: str dtype: str
@@ -85,7 +84,7 @@ class Column:
def string_size(self) -> int: def string_size(self) -> int:
if not self.is_string(): if not self.is_string():
raise DbtRuntimeError("Called string_size() on non-string field!") raise RuntimeException("Called string_size() on non-string field!")
if self.dtype == "text" or self.char_size is None: if self.dtype == "text" or self.char_size is None:
# char_size should never be None. Handle it reasonably just in case # char_size should never be None. Handle it reasonably just in case
@@ -124,7 +123,7 @@ class Column:
def from_description(cls, name: str, raw_data_type: str) -> "Column": def from_description(cls, name: str, raw_data_type: str) -> "Column":
match = re.match(r"([^(]+)(\([^)]+\))?", raw_data_type) match = re.match(r"([^(]+)(\([^)]+\))?", raw_data_type)
if match is None: if match is None:
raise DbtRuntimeError(f'Could not interpret data type "{raw_data_type}"') raise RuntimeException(f'Could not interpret data type "{raw_data_type}"')
data_type, size_info = match.groups() data_type, size_info = match.groups()
char_size = None char_size = None
numeric_precision = None numeric_precision = None
@@ -137,7 +136,7 @@ class Column:
try: try:
char_size = int(parts[0]) char_size = int(parts[0])
except ValueError: except ValueError:
raise DbtRuntimeError( raise RuntimeException(
f'Could not interpret data_type "{raw_data_type}": ' f'Could not interpret data_type "{raw_data_type}": '
f'could not convert "{parts[0]}" to an integer' f'could not convert "{parts[0]}" to an integer'
) )
@@ -145,14 +144,14 @@ class Column:
try: try:
numeric_precision = int(parts[0]) numeric_precision = int(parts[0])
except ValueError: except ValueError:
raise DbtRuntimeError( raise RuntimeException(
f'Could not interpret data_type "{raw_data_type}": ' f'Could not interpret data_type "{raw_data_type}": '
f'could not convert "{parts[0]}" to an integer' f'could not convert "{parts[0]}" to an integer'
) )
try: try:
numeric_scale = int(parts[1]) numeric_scale = int(parts[1])
except ValueError: except ValueError:
raise DbtRuntimeError( raise RuntimeException(
f'Could not interpret data_type "{raw_data_type}": ' f'Could not interpret data_type "{raw_data_type}": '
f'could not convert "{parts[1]}" to an integer' f'could not convert "{parts[1]}" to an integer'
) )

View File

@@ -1,25 +1,10 @@
import abc import abc
import os import os
from time import sleep
import sys
import traceback
# multiprocessing.RLock is a function returning this type # multiprocessing.RLock is a function returning this type
from multiprocessing.synchronize import RLock from multiprocessing.synchronize import RLock
from threading import get_ident from threading import get_ident
from typing import ( from typing import Dict, Tuple, Hashable, Optional, ContextManager, List
Any,
Dict,
Tuple,
Hashable,
Optional,
ContextManager,
List,
Type,
Union,
Iterable,
Callable,
)
import agate import agate
@@ -36,24 +21,18 @@ from dbt.contracts.graph.manifest import Manifest
from dbt.adapters.base.query_headers import ( from dbt.adapters.base.query_headers import (
MacroQueryStringSetter, MacroQueryStringSetter,
) )
from dbt.events import AdapterLogger
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import ( from dbt.events.types import (
NewConnection, NewConnection,
ConnectionReused, ConnectionReused,
ConnectionLeftOpenInCleanup,
ConnectionLeftOpen, ConnectionLeftOpen,
ConnectionClosedInCleanup, ConnectionLeftOpen2,
ConnectionClosed, ConnectionClosed,
ConnectionClosed2,
Rollback, Rollback,
RollbackFailed, RollbackFailed,
) )
from dbt.events.contextvars import get_node_info
from dbt import flags from dbt import flags
from dbt.utils import cast_to_str
SleepTime = Union[int, float] # As taken by time.sleep.
AdapterHandle = Any # Adapter connection handle objects can be any class.
class BaseConnectionManager(metaclass=abc.ABCMeta): class BaseConnectionManager(metaclass=abc.ABCMeta):
@@ -91,13 +70,13 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
key = self.get_thread_identifier() key = self.get_thread_identifier()
with self.lock: with self.lock:
if key not in self.thread_connections: if key not in self.thread_connections:
raise dbt.exceptions.InvalidConnectionError(key, list(self.thread_connections)) raise dbt.exceptions.InvalidConnectionException(key, list(self.thread_connections))
return self.thread_connections[key] return self.thread_connections[key]
def set_thread_connection(self, conn: Connection) -> None: def set_thread_connection(self, conn: Connection) -> None:
key = self.get_thread_identifier() key = self.get_thread_identifier()
if key in self.thread_connections: if key in self.thread_connections:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
"In set_thread_connection, existing connection exists for {}" "In set_thread_connection, existing connection exists for {}"
) )
self.thread_connections[key] = conn self.thread_connections[key] = conn
@@ -137,148 +116,57 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
:return: A context manager that handles exceptions raised by the :return: A context manager that handles exceptions raised by the
underlying database. underlying database.
""" """
raise dbt.exceptions.NotImplementedError( raise dbt.exceptions.NotImplementedException(
"`exception_handler` is not implemented for this adapter!" "`exception_handler` is not implemented for this adapter!"
) )
def set_connection_name(self, name: Optional[str] = None) -> Connection: def set_connection_name(self, name: Optional[str] = None) -> Connection:
"""Called by 'acquire_connection' in BaseAdapter, which is called by conn_name: str
'connection_named', called by 'connection_for(node)'. if name is None:
Creates a connection for this thread if one doesn't already # if a name isn't specified, we'll re-use a single handle
exist, and will rename an existing connection.""" # named 'master'
conn_name = "master"
else:
if not isinstance(name, str):
raise dbt.exceptions.CompilerException(
f"For connection name, got {name} - not a string!"
)
assert isinstance(name, str)
conn_name = name
conn_name: str = "master" if name is None else name
# Get a connection for this thread
conn = self.get_if_exists() conn = self.get_if_exists()
if conn and conn.name == conn_name and conn.state == "open":
# Found a connection and nothing to do, so just return it
return conn
if conn is None: if conn is None:
# Create a new connection
conn = Connection( conn = Connection(
type=Identifier(self.TYPE), type=Identifier(self.TYPE),
name=conn_name, name=None,
state=ConnectionState.INIT, state=ConnectionState.INIT,
transaction_open=False, transaction_open=False,
handle=None, handle=None,
credentials=self.profile.credentials, credentials=self.profile.credentials,
) )
conn.handle = LazyHandle(self.open)
# Add the connection to thread_connections for this thread
self.set_thread_connection(conn) self.set_thread_connection(conn)
fire_event(
NewConnection(conn_name=conn_name, conn_type=self.TYPE, node_info=get_node_info())
)
else: # existing connection either wasn't open or didn't have the right name
if conn.state != "open":
conn.handle = LazyHandle(self.open)
if conn.name != conn_name:
orig_conn_name: str = conn.name or ""
conn.name = conn_name
fire_event(ConnectionReused(orig_conn_name=orig_conn_name, conn_name=conn_name))
if conn.name == conn_name and conn.state == "open":
return conn return conn
@classmethod fire_event(NewConnection(conn_name=conn_name, conn_type=self.TYPE))
def retry_connection(
cls,
connection: Connection,
connect: Callable[[], AdapterHandle],
logger: AdapterLogger,
retryable_exceptions: Iterable[Type[Exception]],
retry_limit: int = 1,
retry_timeout: Union[Callable[[int], SleepTime], SleepTime] = 1,
_attempts: int = 0,
) -> Connection:
"""Given a Connection, set its handle by calling connect.
The calls to connect will be retried up to retry_limit times to deal with transient if conn.state == "open":
connection errors. By default, one retry will be attempted if retryable_exceptions is set. fire_event(ConnectionReused(conn_name=conn_name))
else:
conn.handle = LazyHandle(self.open)
:param Connection connection: An instance of a Connection that needs a handle to be set, conn.name = conn_name
usually when attempting to open it. return conn
:param connect: A callable that returns the appropiate connection handle for a
given adapter. This callable will be retried retry_limit times if a subclass of any
Exception in retryable_exceptions is raised by connect.
:type connect: Callable[[], AdapterHandle]
:param AdapterLogger logger: A logger to emit messages on retry attempts or errors. When
handling expected errors, we call debug, and call warning on unexpected errors or when
all retry attempts have been exhausted.
:param retryable_exceptions: An iterable of exception classes that if raised by
connect should trigger a retry.
:type retryable_exceptions: Iterable[Type[Exception]]
:param int retry_limit: How many times to retry the call to connect. If this limit
is exceeded before a successful call, a FailedToConnectError will be raised.
Must be non-negative.
:param retry_timeout: Time to wait between attempts to connect. Can also take a
Callable that takes the number of attempts so far, beginning at 0, and returns an int
or float to be passed to time.sleep.
:type retry_timeout: Union[Callable[[int], SleepTime], SleepTime] = 1
:param int _attempts: Parameter used to keep track of the number of attempts in calling the
connect function across recursive calls. Passed as an argument to retry_timeout if it
is a Callable. This parameter should not be set by the initial caller.
:raises dbt.exceptions.FailedToConnectError: Upon exhausting all retry attempts without
successfully acquiring a handle.
:return: The given connection with its appropriate state and handle attributes set
depending on whether we successfully acquired a handle or not.
"""
timeout = retry_timeout(_attempts) if callable(retry_timeout) else retry_timeout
if timeout < 0:
raise dbt.exceptions.FailedToConnectError(
"retry_timeout cannot be negative or return a negative time."
)
if retry_limit < 0 or retry_limit > sys.getrecursionlimit():
# This guard is not perfect others may add to the recursion limit (e.g. built-ins).
connection.handle = None
connection.state = ConnectionState.FAIL
raise dbt.exceptions.FailedToConnectError("retry_limit cannot be negative")
try:
connection.handle = connect()
connection.state = ConnectionState.OPEN
return connection
except tuple(retryable_exceptions) as e:
if retry_limit <= 0:
connection.handle = None
connection.state = ConnectionState.FAIL
raise dbt.exceptions.FailedToConnectError(str(e))
logger.debug(
f"Got a retryable error when attempting to open a {cls.TYPE} connection.\n"
f"{retry_limit} attempts remaining. Retrying in {timeout} seconds.\n"
f"Error:\n{e}"
)
sleep(timeout)
return cls.retry_connection(
connection=connection,
connect=connect,
logger=logger,
retry_limit=retry_limit - 1,
retry_timeout=retry_timeout,
retryable_exceptions=retryable_exceptions,
_attempts=_attempts + 1,
)
except Exception as e:
connection.handle = None
connection.state = ConnectionState.FAIL
raise dbt.exceptions.FailedToConnectError(str(e))
@abc.abstractmethod @abc.abstractmethod
def cancel_open(self) -> Optional[List[str]]: def cancel_open(self) -> Optional[List[str]]:
"""Cancel all open connections on the adapter. (passable)""" """Cancel all open connections on the adapter. (passable)"""
raise dbt.exceptions.NotImplementedError( raise dbt.exceptions.NotImplementedException(
"`cancel_open` is not implemented for this adapter!" "`cancel_open` is not implemented for this adapter!"
) )
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def open(cls, connection: Connection) -> Connection: def open(cls, connection: Connection) -> Connection:
"""Open the given connection on the adapter and return it. """Open the given connection on the adapter and return it.
@@ -288,7 +176,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
This should be thread-safe, or hold the lock if necessary. The given This should be thread-safe, or hold the lock if necessary. The given
connection should not be in either in_use or available. connection should not be in either in_use or available.
""" """
raise dbt.exceptions.NotImplementedError("`open` is not implemented for this adapter!") raise dbt.exceptions.NotImplementedException("`open` is not implemented for this adapter!")
def release(self) -> None: def release(self) -> None:
with self.lock: with self.lock:
@@ -309,9 +197,9 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
with self.lock: with self.lock:
for connection in self.thread_connections.values(): for connection in self.thread_connections.values():
if connection.state not in {"closed", "init"}: if connection.state not in {"closed", "init"}:
fire_event(ConnectionLeftOpenInCleanup(conn_name=cast_to_str(connection.name))) fire_event(ConnectionLeftOpen(conn_name=connection.name))
else: else:
fire_event(ConnectionClosedInCleanup(conn_name=cast_to_str(connection.name))) fire_event(ConnectionClosed(conn_name=connection.name))
self.close(connection) self.close(connection)
# garbage collect these connections # garbage collect these connections
@@ -320,12 +208,16 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
@abc.abstractmethod @abc.abstractmethod
def begin(self) -> None: def begin(self) -> None:
"""Begin a transaction. (passable)""" """Begin a transaction. (passable)"""
raise dbt.exceptions.NotImplementedError("`begin` is not implemented for this adapter!") raise dbt.exceptions.NotImplementedException(
"`begin` is not implemented for this adapter!"
)
@abc.abstractmethod @abc.abstractmethod
def commit(self) -> None: def commit(self) -> None:
"""Commit a transaction. (passable)""" """Commit a transaction. (passable)"""
raise dbt.exceptions.NotImplementedError("`commit` is not implemented for this adapter!") raise dbt.exceptions.NotImplementedException(
"`commit` is not implemented for this adapter!"
)
@classmethod @classmethod
def _rollback_handle(cls, connection: Connection) -> None: def _rollback_handle(cls, connection: Connection) -> None:
@@ -333,40 +225,28 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
try: try:
connection.handle.rollback() connection.handle.rollback()
except Exception: except Exception:
fire_event( fire_event(RollbackFailed(conn_name=connection.name))
RollbackFailed(
conn_name=cast_to_str(connection.name),
exc_info=traceback.format_exc(),
node_info=get_node_info(),
)
)
@classmethod @classmethod
def _close_handle(cls, connection: Connection) -> None: def _close_handle(cls, connection: Connection) -> None:
"""Perform the actual close operation.""" """Perform the actual close operation."""
# On windows, sometimes connection handles don't have a close() attr. # On windows, sometimes connection handles don't have a close() attr.
if hasattr(connection.handle, "close"): if hasattr(connection.handle, "close"):
fire_event( fire_event(ConnectionClosed2(conn_name=connection.name))
ConnectionClosed(conn_name=cast_to_str(connection.name), node_info=get_node_info())
)
connection.handle.close() connection.handle.close()
else: else:
fire_event( fire_event(ConnectionLeftOpen2(conn_name=connection.name))
ConnectionLeftOpen(
conn_name=cast_to_str(connection.name), node_info=get_node_info()
)
)
@classmethod @classmethod
def _rollback(cls, connection: Connection) -> None: def _rollback(cls, connection: Connection) -> None:
"""Roll back the given connection.""" """Roll back the given connection."""
if connection.transaction_open is False: if connection.transaction_open is False:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
f"Tried to rollback transaction on connection " f"Tried to rollback transaction on connection "
f'"{connection.name}", but it does not have one open!' f'"{connection.name}", but it does not have one open!'
) )
fire_event(Rollback(conn_name=cast_to_str(connection.name), node_info=get_node_info())) fire_event(Rollback(conn_name=connection.name))
cls._rollback_handle(connection) cls._rollback_handle(connection)
connection.transaction_open = False connection.transaction_open = False
@@ -378,7 +258,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
return connection return connection
if connection.transaction_open and connection.handle: if connection.transaction_open and connection.handle:
fire_event(Rollback(conn_name=cast_to_str(connection.name), node_info=get_node_info())) fire_event(Rollback(conn_name=connection.name))
cls._rollback_handle(connection) cls._rollback_handle(connection)
connection.transaction_open = False connection.transaction_open = False
@@ -411,4 +291,6 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
:return: A tuple of the query status and results (empty if fetch=False). :return: A tuple of the query status and results (empty if fetch=False).
:rtype: Tuple[AdapterResponse, agate.Table] :rtype: Tuple[AdapterResponse, agate.Table]
""" """
raise dbt.exceptions.NotImplementedError("`execute` is not implemented for this adapter!") raise dbt.exceptions.NotImplementedException(
"`execute` is not implemented for this adapter!"
)

View File

@@ -2,7 +2,6 @@ import abc
from concurrent.futures import as_completed, Future from concurrent.futures import as_completed, Future
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime from datetime import datetime
import time
from itertools import chain from itertools import chain
from typing import ( from typing import (
Optional, Optional,
@@ -15,6 +14,7 @@ from typing import (
List, List,
Mapping, Mapping,
Iterator, Iterator,
Union,
Set, Set,
) )
@@ -22,20 +22,13 @@ import agate
import pytz import pytz
from dbt.exceptions import ( from dbt.exceptions import (
DbtInternalError, raise_database_error,
MacroArgTypeError, raise_compiler_error,
MacroResultError, invalid_type_error,
QuoteConfigTypeError, get_relation_returned_multiple_results,
NotImplementedError, InternalException,
NullRelationCacheAttemptedError, NotImplementedException,
NullRelationDropAttemptedError, RuntimeException,
RelationReturnedMultipleResultsError,
RenameToNoneAttemptedError,
DbtRuntimeError,
SnapshotTargetIncompleteError,
SnapshotTargetNotSnapshotTableError,
UnexpectedNullError,
UnexpectedNonTimestampError,
) )
from dbt.adapters.protocol import ( from dbt.adapters.protocol import (
@@ -44,17 +37,13 @@ from dbt.adapters.protocol import (
) )
from dbt.clients.agate_helper import empty_table, merge_tables, table_from_rows from dbt.clients.agate_helper import empty_table, merge_tables, table_from_rows
from dbt.clients.jinja import MacroGenerator from dbt.clients.jinja import MacroGenerator
from dbt.contracts.graph.compiled import CompileResultNode, CompiledSeedNode
from dbt.contracts.graph.manifest import Manifest, MacroManifest from dbt.contracts.graph.manifest import Manifest, MacroManifest
from dbt.contracts.graph.nodes import ResultNode from dbt.contracts.graph.parsed import ParsedSeedNode
from dbt.events.functions import fire_event, warn_or_error from dbt.exceptions import warn_or_error
from dbt.events.types import ( from dbt.events.functions import fire_event
CacheMiss, from dbt.events.types import CacheMiss, ListRelations
ListRelations, from dbt.utils import filter_null_values, executor
CodeExecution,
CodeExecutionStatus,
CatalogGenerationError,
)
from dbt.utils import filter_null_values, executor, cast_to_str
from dbt.adapters.base.connections import Connection, AdapterResponse from dbt.adapters.base.connections import Connection, AdapterResponse
from dbt.adapters.base.meta import AdapterMeta, available from dbt.adapters.base.meta import AdapterMeta, available
@@ -65,8 +54,10 @@ from dbt.adapters.base.relation import (
SchemaSearchMap, SchemaSearchMap,
) )
from dbt.adapters.base import Column as BaseColumn from dbt.adapters.base import Column as BaseColumn
from dbt.adapters.base import Credentials from dbt.adapters.cache import RelationsCache, _make_key
from dbt.adapters.cache import RelationsCache, _make_ref_key_msg
SeedModel = Union[ParsedSeedNode, CompiledSeedNode]
GET_CATALOG_MACRO_NAME = "get_catalog" GET_CATALOG_MACRO_NAME = "get_catalog"
@@ -75,7 +66,7 @@ FRESHNESS_MACRO_NAME = "collect_freshness"
def _expect_row_value(key: str, row: agate.Row): def _expect_row_value(key: str, row: agate.Row):
if key not in row.keys(): if key not in row.keys():
raise DbtInternalError( raise InternalException(
'Got a row without "{}" column, columns: {}'.format(key, row.keys()) 'Got a row without "{}" column, columns: {}'.format(key, row.keys())
) )
return row[key] return row[key]
@@ -104,10 +95,18 @@ def _utc(dt: Optional[datetime], source: BaseRelation, field_name: str) -> datet
assume the datetime is already for UTC and add the timezone. assume the datetime is already for UTC and add the timezone.
""" """
if dt is None: if dt is None:
raise UnexpectedNullError(field_name, source) raise raise_database_error(
"Expected a non-null value when querying field '{}' of table "
" {} but received value 'null' instead".format(field_name, source)
)
elif not hasattr(dt, "tzinfo"): elif not hasattr(dt, "tzinfo"):
raise UnexpectedNonTimestampError(field_name, source, dt) raise raise_database_error(
"Expected a timestamp value when querying field '{}' of table "
"{} but received value of type '{}' instead".format(
field_name, source, type(dt).__name__
)
)
elif dt.tzinfo: elif dt.tzinfo:
return dt.astimezone(pytz.UTC) return dt.astimezone(pytz.UTC)
@@ -122,35 +121,6 @@ def _relation_name(rel: Optional[BaseRelation]) -> str:
return str(rel) return str(rel)
def log_code_execution(code_execution_function):
# decorator to log code and execution time
if code_execution_function.__name__ != "submit_python_job":
raise ValueError("this should be only used to log submit_python_job now")
def execution_with_log(*args):
self = args[0]
connection_name = self.connections.get_thread_connection().name
fire_event(CodeExecution(conn_name=connection_name, code_content=args[2]))
start_time = time.time()
response = code_execution_function(*args)
fire_event(
CodeExecutionStatus(
status=response._message, elapsed=round((time.time() - start_time), 2)
)
)
return response
return execution_with_log
class PythonJobHelper:
def __init__(self, parsed_model: Dict, credential: Credentials) -> None:
raise NotImplementedError("PythonJobHelper is not implemented yet")
def submit(self, compiled_code: str) -> Any:
raise NotImplementedError("PythonJobHelper submit function is not implemented yet")
class BaseAdapter(metaclass=AdapterMeta): class BaseAdapter(metaclass=AdapterMeta):
"""The BaseAdapter provides an abstract base class for adapters. """The BaseAdapter provides an abstract base class for adapters.
@@ -160,15 +130,9 @@ class BaseAdapter(metaclass=AdapterMeta):
methods are marked with a (passable) in their docstrings. Check docstrings methods are marked with a (passable) in their docstrings. Check docstrings
for type information, etc. for type information, etc.
To implement a macro, implement "${adapter_type}__${macro_name}" in the To implement a macro, implement "${adapter_type}__${macro_name}". in the
adapter's internal project. adapter's internal project.
To invoke a method in an adapter macro, call it on the 'adapter' Jinja
object using dot syntax.
To invoke a method in model code, add the @available decorator atop a method
declaration. Methods are invoked as macros.
Methods: Methods:
- exception_handler - exception_handler
- date_function - date_function
@@ -189,7 +153,6 @@ class BaseAdapter(metaclass=AdapterMeta):
- convert_datetime_type - convert_datetime_type
- convert_date_type - convert_date_type
- convert_time_type - convert_time_type
- standardize_grants_dict
Macros: Macros:
- get_catalog - get_catalog
@@ -237,7 +200,9 @@ class BaseAdapter(metaclass=AdapterMeta):
return conn.name return conn.name
@contextmanager @contextmanager
def connection_named(self, name: str, node: Optional[ResultNode] = None) -> Iterator[None]: def connection_named(
self, name: str, node: Optional[CompileResultNode] = None
) -> Iterator[None]:
try: try:
if self.connections.query_header is not None: if self.connections.query_header is not None:
self.connections.query_header.set(name, node) self.connections.query_header.set(name, node)
@@ -249,7 +214,7 @@ class BaseAdapter(metaclass=AdapterMeta):
self.connections.query_header.reset() self.connections.query_header.reset()
@contextmanager @contextmanager
def connection_for(self, node: ResultNode) -> Iterator[None]: def connection_for(self, node: CompileResultNode) -> Iterator[None]:
with self.connection_named(node.unique_id, node): with self.connection_named(node.unique_id, node):
yield yield
@@ -312,9 +277,7 @@ class BaseAdapter(metaclass=AdapterMeta):
from dbt.parser.manifest import ManifestLoader from dbt.parser.manifest import ManifestLoader
manifest = ManifestLoader.load_macros( manifest = ManifestLoader.load_macros(
self.config, self.config, self.connections.set_query_header, base_macros_only=base_macros_only
self.connections.set_query_header,
base_macros_only=base_macros_only,
) )
# TODO CT-211 # TODO CT-211
self._macro_manifest_lazy = manifest # type: ignore[assignment] self._macro_manifest_lazy = manifest # type: ignore[assignment]
@@ -333,11 +296,7 @@ class BaseAdapter(metaclass=AdapterMeta):
if (database, schema) not in self.cache: if (database, schema) not in self.cache:
fire_event( fire_event(
CacheMiss( CacheMiss(conn_name=self.nice_connection_name(), database=database, schema=schema)
conn_name=self.nice_connection_name(),
database=cast_to_str(database),
schema=schema,
)
) )
return False return False
else: else:
@@ -364,7 +323,7 @@ class BaseAdapter(metaclass=AdapterMeta):
lowercase strings. lowercase strings.
""" """
info_schema_name_map = SchemaSearchMap() info_schema_name_map = SchemaSearchMap()
nodes: Iterator[ResultNode] = chain( nodes: Iterator[CompileResultNode] = chain(
[ [
node node
for node in manifest.nodes.values() for node in manifest.nodes.values()
@@ -415,10 +374,7 @@ class BaseAdapter(metaclass=AdapterMeta):
self.cache.update_schemas(cache_update) self.cache.update_schemas(cache_update)
def set_relations_cache( def set_relations_cache(
self, self, manifest: Manifest, clear: bool = False, required_schemas: Set[BaseRelation] = None
manifest: Manifest,
clear: bool = False,
required_schemas: Set[BaseRelation] = None,
) -> None: ) -> None:
"""Run a query that gets a populated cache of the relations in the """Run a query that gets a populated cache of the relations in the
database and set the cache on this adapter. database and set the cache on this adapter.
@@ -433,7 +389,7 @@ class BaseAdapter(metaclass=AdapterMeta):
"""Cache a new relation in dbt. It will show up in `list relations`.""" """Cache a new relation in dbt. It will show up in `list relations`."""
if relation is None: if relation is None:
name = self.nice_connection_name() name = self.nice_connection_name()
raise NullRelationCacheAttemptedError(name) raise_compiler_error("Attempted to cache a null relation for {}".format(name))
self.cache.add(relation) self.cache.add(relation)
# so jinja doesn't render things # so jinja doesn't render things
return "" return ""
@@ -445,7 +401,7 @@ class BaseAdapter(metaclass=AdapterMeta):
""" """
if relation is None: if relation is None:
name = self.nice_connection_name() name = self.nice_connection_name()
raise NullRelationDropAttemptedError(name) raise_compiler_error("Attempted to drop a null relation for {}".format(name))
self.cache.drop(relation) self.cache.drop(relation)
return "" return ""
@@ -462,7 +418,9 @@ class BaseAdapter(metaclass=AdapterMeta):
name = self.nice_connection_name() name = self.nice_connection_name()
src_name = _relation_name(from_relation) src_name = _relation_name(from_relation)
dst_name = _relation_name(to_relation) dst_name = _relation_name(to_relation)
raise RenameToNoneAttemptedError(src_name, dst_name, name) raise_compiler_error(
"Attempted to rename {} to {} for {}".format(src_name, dst_name, name)
)
self.cache.rename(from_relation, to_relation) self.cache.rename(from_relation, to_relation)
return "" return ""
@@ -470,16 +428,14 @@ class BaseAdapter(metaclass=AdapterMeta):
### ###
# Abstract methods for database-specific values, attributes, and types # Abstract methods for database-specific values, attributes, and types
### ###
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def date_function(cls) -> str: def date_function(cls) -> str:
"""Get the date function used by this adapter's database.""" """Get the date function used by this adapter's database."""
raise NotImplementedError("`date_function` is not implemented for this adapter!") raise NotImplementedException("`date_function` is not implemented for this adapter!")
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def is_cancelable(cls) -> bool: def is_cancelable(cls) -> bool:
raise NotImplementedError("`is_cancelable` is not implemented for this adapter!") raise NotImplementedException("`is_cancelable` is not implemented for this adapter!")
### ###
# Abstract methods about schemas # Abstract methods about schemas
@@ -487,7 +443,7 @@ class BaseAdapter(metaclass=AdapterMeta):
@abc.abstractmethod @abc.abstractmethod
def list_schemas(self, database: str) -> List[str]: def list_schemas(self, database: str) -> List[str]:
"""Get a list of existing schemas in database""" """Get a list of existing schemas in database"""
raise NotImplementedError("`list_schemas` is not implemented for this adapter!") raise NotImplementedException("`list_schemas` is not implemented for this adapter!")
@available.parse(lambda *a, **k: False) @available.parse(lambda *a, **k: False)
def check_schema_exists(self, database: str, schema: str) -> bool: def check_schema_exists(self, database: str, schema: str) -> bool:
@@ -510,13 +466,13 @@ class BaseAdapter(metaclass=AdapterMeta):
*Implementors must call self.cache.drop() to preserve cache state!* *Implementors must call self.cache.drop() to preserve cache state!*
""" """
raise NotImplementedError("`drop_relation` is not implemented for this adapter!") raise NotImplementedException("`drop_relation` is not implemented for this adapter!")
@abc.abstractmethod @abc.abstractmethod
@available.parse_none @available.parse_none
def truncate_relation(self, relation: BaseRelation) -> None: def truncate_relation(self, relation: BaseRelation) -> None:
"""Truncate the given relation.""" """Truncate the given relation."""
raise NotImplementedError("`truncate_relation` is not implemented for this adapter!") raise NotImplementedException("`truncate_relation` is not implemented for this adapter!")
@abc.abstractmethod @abc.abstractmethod
@available.parse_none @available.parse_none
@@ -525,13 +481,15 @@ class BaseAdapter(metaclass=AdapterMeta):
Implementors must call self.cache.rename() to preserve cache state. Implementors must call self.cache.rename() to preserve cache state.
""" """
raise NotImplementedError("`rename_relation` is not implemented for this adapter!") raise NotImplementedException("`rename_relation` is not implemented for this adapter!")
@abc.abstractmethod @abc.abstractmethod
@available.parse_list @available.parse_list
def get_columns_in_relation(self, relation: BaseRelation) -> List[BaseColumn]: def get_columns_in_relation(self, relation: BaseRelation) -> List[BaseColumn]:
"""Get a list of the columns in the given Relation.""" """Get a list of the columns in the given Relation."""
raise NotImplementedError("`get_columns_in_relation` is not implemented for this adapter!") raise NotImplementedException(
"`get_columns_in_relation` is not implemented for this adapter!"
)
@available.deprecated("get_columns_in_relation", lambda *a, **k: []) @available.deprecated("get_columns_in_relation", lambda *a, **k: [])
def get_columns_in_table(self, schema: str, identifier: str) -> List[BaseColumn]: def get_columns_in_table(self, schema: str, identifier: str) -> List[BaseColumn]:
@@ -553,7 +511,7 @@ class BaseAdapter(metaclass=AdapterMeta):
:param self.Relation current: A relation that currently exists in the :param self.Relation current: A relation that currently exists in the
database with columns of unspecified types. database with columns of unspecified types.
""" """
raise NotImplementedError( raise NotImplementedException(
"`expand_target_column_types` is not implemented for this adapter!" "`expand_target_column_types` is not implemented for this adapter!"
) )
@@ -568,37 +526,10 @@ class BaseAdapter(metaclass=AdapterMeta):
:return: The relations in schema :return: The relations in schema
:rtype: List[self.Relation] :rtype: List[self.Relation]
""" """
raise NotImplementedError( raise NotImplementedException(
"`list_relations_without_caching` is not implemented for this adapter!" "`list_relations_without_caching` is not implemented for this " "adapter!"
) )
###
# Methods about grants
###
@available
def standardize_grants_dict(self, grants_table: agate.Table) -> dict:
"""Translate the result of `show grants` (or equivalent) to match the
grants which a user would configure in their project.
Ideally, the SQL to show grants should also be filtering:
filter OUT any grants TO the current user/role (e.g. OWNERSHIP).
If that's not possible in SQL, it can be done in this method instead.
:param grants_table: An agate table containing the query result of
the SQL returned by get_show_grant_sql
:return: A standardized dictionary matching the `grants` config
:rtype: dict
"""
grants_dict: Dict[str, List[str]] = {}
for row in grants_table:
grantee = row["grantee"]
privilege = row["privilege_type"]
if privilege in grants_dict.keys():
grants_dict[privilege].append(grantee)
else:
grants_dict.update({privilege: [grantee]})
return grants_dict
### ###
# Provided methods about relations # Provided methods about relations
### ###
@@ -610,7 +541,7 @@ class BaseAdapter(metaclass=AdapterMeta):
to_relation. to_relation.
""" """
if not isinstance(from_relation, self.Relation): if not isinstance(from_relation, self.Relation):
raise MacroArgTypeError( invalid_type_error(
method_name="get_missing_columns", method_name="get_missing_columns",
arg_name="from_relation", arg_name="from_relation",
got_value=from_relation, got_value=from_relation,
@@ -618,7 +549,7 @@ class BaseAdapter(metaclass=AdapterMeta):
) )
if not isinstance(to_relation, self.Relation): if not isinstance(to_relation, self.Relation):
raise MacroArgTypeError( invalid_type_error(
method_name="get_missing_columns", method_name="get_missing_columns",
arg_name="to_relation", arg_name="to_relation",
got_value=to_relation, got_value=to_relation,
@@ -639,11 +570,11 @@ class BaseAdapter(metaclass=AdapterMeta):
expected columns. expected columns.
:param Relation relation: The relation to check :param Relation relation: The relation to check
:raises InvalidMacroArgType: If the columns are :raises CompilationException: If the columns are
incorrect. incorrect.
""" """
if not isinstance(relation, self.Relation): if not isinstance(relation, self.Relation):
raise MacroArgTypeError( invalid_type_error(
method_name="valid_snapshot_target", method_name="valid_snapshot_target",
arg_name="relation", arg_name="relation",
got_value=relation, got_value=relation,
@@ -664,16 +595,24 @@ class BaseAdapter(metaclass=AdapterMeta):
if missing: if missing:
if extra: if extra:
raise SnapshotTargetIncompleteError(extra, missing) msg = (
'Snapshot target has ("{}") but not ("{}") - is it an '
"unmigrated previous version archive?".format(
'", "'.join(extra), '", "'.join(missing)
)
)
else: else:
raise SnapshotTargetNotSnapshotTableError(missing) msg = 'Snapshot target is not a snapshot table (missing "{}")'.format(
'", "'.join(missing)
)
raise_compiler_error(msg)
@available.parse_none @available.parse_none
def expand_target_column_types( def expand_target_column_types(
self, from_relation: BaseRelation, to_relation: BaseRelation self, from_relation: BaseRelation, to_relation: BaseRelation
) -> None: ) -> None:
if not isinstance(from_relation, self.Relation): if not isinstance(from_relation, self.Relation):
raise MacroArgTypeError( invalid_type_error(
method_name="expand_target_column_types", method_name="expand_target_column_types",
arg_name="from_relation", arg_name="from_relation",
got_value=from_relation, got_value=from_relation,
@@ -681,7 +620,7 @@ class BaseAdapter(metaclass=AdapterMeta):
) )
if not isinstance(to_relation, self.Relation): if not isinstance(to_relation, self.Relation):
raise MacroArgTypeError( invalid_type_error(
method_name="expand_target_column_types", method_name="expand_target_column_types",
arg_name="to_relation", arg_name="to_relation",
got_value=to_relation, got_value=to_relation,
@@ -695,10 +634,7 @@ class BaseAdapter(metaclass=AdapterMeta):
return self.cache.get_relations(database, schema) return self.cache.get_relations(database, schema)
schema_relation = self.Relation.create( schema_relation = self.Relation.create(
database=database, database=database, schema=schema, identifier="", quote_policy=self.config.quoting
schema=schema,
identifier="",
quote_policy=self.config.quoting,
).without_identifier() ).without_identifier()
# we can't build the relations cache because we don't have a # we can't build the relations cache because we don't have a
@@ -706,9 +642,7 @@ class BaseAdapter(metaclass=AdapterMeta):
relations = self.list_relations_without_caching(schema_relation) relations = self.list_relations_without_caching(schema_relation)
fire_event( fire_event(
ListRelations( ListRelations(
database=cast_to_str(database), database=database, schema=schema, relations=[_make_key(x) for x in relations]
schema=schema,
relations=[_make_ref_key_msg(x) for x in relations],
) )
) )
@@ -763,7 +697,7 @@ class BaseAdapter(metaclass=AdapterMeta):
"schema": schema, "schema": schema,
"database": database, "database": database,
} }
raise RelationReturnedMultipleResultsError(kwargs, matches) get_relation_returned_multiple_results(kwargs, matches)
elif matches: elif matches:
return matches[0] return matches[0]
@@ -785,20 +719,19 @@ class BaseAdapter(metaclass=AdapterMeta):
@available.parse_none @available.parse_none
def create_schema(self, relation: BaseRelation): def create_schema(self, relation: BaseRelation):
"""Create the given schema if it does not exist.""" """Create the given schema if it does not exist."""
raise NotImplementedError("`create_schema` is not implemented for this adapter!") raise NotImplementedException("`create_schema` is not implemented for this adapter!")
@abc.abstractmethod @abc.abstractmethod
@available.parse_none @available.parse_none
def drop_schema(self, relation: BaseRelation): def drop_schema(self, relation: BaseRelation):
"""Drop the given schema (and everything in it) if it exists.""" """Drop the given schema (and everything in it) if it exists."""
raise NotImplementedError("`drop_schema` is not implemented for this adapter!") raise NotImplementedException("`drop_schema` is not implemented for this adapter!")
@available @available
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def quote(cls, identifier: str) -> str: def quote(cls, identifier: str) -> str:
"""Quote the given identifier, as appropriate for the database.""" """Quote the given identifier, as appropriate for the database."""
raise NotImplementedError("`quote` is not implemented for this adapter!") raise NotImplementedException("`quote` is not implemented for this adapter!")
@available @available
def quote_as_configured(self, identifier: str, quote_key: str) -> str: def quote_as_configured(self, identifier: str, quote_key: str) -> str:
@@ -827,7 +760,10 @@ class BaseAdapter(metaclass=AdapterMeta):
elif quote_config is None: elif quote_config is None:
pass pass
else: else:
raise QuoteConfigTypeError(quote_config) raise_compiler_error(
f'The seed configuration value of "quote_columns" has an '
f"invalid type {type(quote_config)}"
)
if quote_columns: if quote_columns:
return self.quote(column) return self.quote(column)
@@ -838,8 +774,7 @@ class BaseAdapter(metaclass=AdapterMeta):
# Conversions: These must be implemented by concrete implementations, for # Conversions: These must be implemented by concrete implementations, for
# converting agate types into their sql equivalents. # converting agate types into their sql equivalents.
### ###
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def convert_text_type(cls, agate_table: agate.Table, col_idx: int) -> str: def convert_text_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the agate.Text """Return the type in the database that best maps to the agate.Text
type for the given agate table and column index. type for the given agate table and column index.
@@ -848,10 +783,9 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_text_type` is not implemented for this adapter!") raise NotImplementedException("`convert_text_type` is not implemented for this adapter!")
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def convert_number_type(cls, agate_table: agate.Table, col_idx: int) -> str: def convert_number_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the agate.Number """Return the type in the database that best maps to the agate.Number
type for the given agate table and column index. type for the given agate table and column index.
@@ -860,10 +794,9 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_number_type` is not implemented for this adapter!") raise NotImplementedException("`convert_number_type` is not implemented for this adapter!")
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def convert_boolean_type(cls, agate_table: agate.Table, col_idx: int) -> str: def convert_boolean_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the agate.Boolean """Return the type in the database that best maps to the agate.Boolean
type for the given agate table and column index. type for the given agate table and column index.
@@ -872,10 +805,11 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_boolean_type` is not implemented for this adapter!") raise NotImplementedException(
"`convert_boolean_type` is not implemented for this adapter!"
)
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def convert_datetime_type(cls, agate_table: agate.Table, col_idx: int) -> str: def convert_datetime_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the agate.DateTime """Return the type in the database that best maps to the agate.DateTime
type for the given agate table and column index. type for the given agate table and column index.
@@ -884,10 +818,11 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_datetime_type` is not implemented for this adapter!") raise NotImplementedException(
"`convert_datetime_type` is not implemented for this adapter!"
)
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def convert_date_type(cls, agate_table: agate.Table, col_idx: int) -> str: def convert_date_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the agate.Date """Return the type in the database that best maps to the agate.Date
type for the given agate table and column index. type for the given agate table and column index.
@@ -896,10 +831,9 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_date_type` is not implemented for this adapter!") raise NotImplementedException("`convert_date_type` is not implemented for this adapter!")
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def convert_time_type(cls, agate_table: agate.Table, col_idx: int) -> str: def convert_time_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the """Return the type in the database that best maps to the
agate.TimeDelta type for the given agate table and column index. agate.TimeDelta type for the given agate table and column index.
@@ -908,7 +842,7 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_time_type` is not implemented for this adapter!") raise NotImplementedException("`convert_time_type` is not implemented for this adapter!")
@available @available
@classmethod @classmethod
@@ -975,7 +909,7 @@ class BaseAdapter(metaclass=AdapterMeta):
else: else:
package_name = 'the "{}" package'.format(project) package_name = 'the "{}" package'.format(project)
raise DbtRuntimeError( raise RuntimeException(
'dbt could not find a macro with the name "{}" in {}'.format( 'dbt could not find a macro with the name "{}" in {}'.format(
macro_name, package_name macro_name, package_name
) )
@@ -1073,7 +1007,11 @@ class BaseAdapter(metaclass=AdapterMeta):
# now we have a 1-row table of the maximum `loaded_at_field` value and # now we have a 1-row table of the maximum `loaded_at_field` value and
# the current time according to the db. # the current time according to the db.
if len(table) != 1 or len(table[0]) != 2: if len(table) != 1 or len(table[0]) != 2:
raise MacroResultError(FRESHNESS_MACRO_NAME, table) raise_compiler_error(
'Got an invalid result from "{}" macro: {}'.format(
FRESHNESS_MACRO_NAME, [tuple(r) for r in table]
)
)
if table[0][0] is None: if table[0][0] is None:
# no records in the table, so really the max_loaded_at was # no records in the table, so really the max_loaded_at was
# infinitely long ago. Just call it 0:00 January 1 year UTC # infinitely long ago. Just call it 0:00 January 1 year UTC
@@ -1150,7 +1088,7 @@ class BaseAdapter(metaclass=AdapterMeta):
elif location == "prepend": elif location == "prepend":
return f"'{value}' || {add_to}" return f"'{value}' || {add_to}"
else: else:
raise DbtRuntimeError(f'Got an unexpected location value of "{location}"') raise RuntimeException(f'Got an unexpected location value of "{location}"')
def get_rows_different_sql( def get_rows_different_sql(
self, self,
@@ -1181,74 +1119,6 @@ class BaseAdapter(metaclass=AdapterMeta):
return sql return sql
@property
def python_submission_helpers(self) -> Dict[str, Type[PythonJobHelper]]:
raise NotImplementedError("python_submission_helpers is not specified")
@property
def default_python_submission_method(self) -> str:
raise NotImplementedError("default_python_submission_method is not specified")
@log_code_execution
def submit_python_job(self, parsed_model: dict, compiled_code: str) -> AdapterResponse:
submission_method = parsed_model["config"].get(
"submission_method", self.default_python_submission_method
)
if submission_method not in self.python_submission_helpers:
raise NotImplementedError(
"Submission method {} is not supported for current adapter".format(
submission_method
)
)
job_helper = self.python_submission_helpers[submission_method](
parsed_model, self.connections.profile.credentials
)
submission_result = job_helper.submit(compiled_code)
# process submission result to generate adapter response
return self.generate_python_submission_response(submission_result)
def generate_python_submission_response(self, submission_result: Any) -> AdapterResponse:
raise NotImplementedError(
"Your adapter need to implement generate_python_submission_response"
)
def valid_incremental_strategies(self):
"""The set of standard builtin strategies which this adapter supports out-of-the-box.
Not used to validate custom strategies defined by end users.
"""
return ["append"]
def builtin_incremental_strategies(self):
return ["append", "delete+insert", "merge", "insert_overwrite"]
@available.parse_none
def get_incremental_strategy_macro(self, model_context, strategy: str):
# Construct macro_name from strategy name
if strategy is None:
strategy = "default"
# validate strategies for this adapter
valid_strategies = self.valid_incremental_strategies()
valid_strategies.append("default")
builtin_strategies = self.builtin_incremental_strategies()
if strategy in builtin_strategies and strategy not in valid_strategies:
raise DbtRuntimeError(
f"The incremental strategy '{strategy}' is not valid for this adapter"
)
strategy = strategy.replace("+", "_")
macro_name = f"get_incremental_{strategy}_sql"
# The model_context should have MacroGenerator callable objects for all macros
if macro_name not in model_context:
raise DbtRuntimeError(
'dbt could not find an incremental strategy macro with the name "{}" in {}'.format(
macro_name, self.config.project_name
)
)
# This returns a callable macro
return model_context[macro_name]
COLUMNS_EQUAL_SQL = """ COLUMNS_EQUAL_SQL = """
with diff_count as ( with diff_count as (
@@ -1296,7 +1166,7 @@ def catch_as_completed(
elif isinstance(exc, KeyboardInterrupt) or not isinstance(exc, Exception): elif isinstance(exc, KeyboardInterrupt) or not isinstance(exc, Exception):
raise exc raise exc
else: else:
warn_or_error(CatalogGenerationError(exc=str(exc))) warn_or_error(f"Encountered an error while generating catalog: {str(exc)}")
# exc is not None, derives from Exception, and isn't ctrl+c # exc is not None, derives from Exception, and isn't ctrl+c
exceptions.append(exc) exceptions.append(exc)
return merge_tables(tables), exceptions return merge_tables(tables), exceptions

View File

@@ -1,7 +1,7 @@
from typing import List, Optional, Type from typing import List, Optional, Type
from dbt.adapters.base import Credentials from dbt.adapters.base import Credentials
from dbt.exceptions import CompilationError from dbt.exceptions import CompilationException
from dbt.adapters.protocol import AdapterProtocol from dbt.adapters.protocol import AdapterProtocol
@@ -11,7 +11,7 @@ def project_name_from_path(include_path: str) -> str:
partial = Project.partial_load(include_path) partial = Project.partial_load(include_path)
if partial.project_name is None: if partial.project_name is None:
raise CompilationError(f"Invalid project at {include_path}: name not set!") raise CompilationException(f"Invalid project at {include_path}: name not set!")
return partial.project_name return partial.project_name

View File

@@ -5,9 +5,9 @@ from dbt.clients.jinja import QueryStringGenerator
from dbt.context.manifest import generate_query_header_context from dbt.context.manifest import generate_query_header_context
from dbt.contracts.connection import AdapterRequiredConfig, QueryComment from dbt.contracts.connection import AdapterRequiredConfig, QueryComment
from dbt.contracts.graph.nodes import ResultNode from dbt.contracts.graph.compiled import CompileResultNode
from dbt.contracts.graph.manifest import Manifest from dbt.contracts.graph.manifest import Manifest
from dbt.exceptions import DbtRuntimeError from dbt.exceptions import RuntimeException
class NodeWrapper: class NodeWrapper:
@@ -48,7 +48,7 @@ class _QueryComment(local):
if isinstance(comment, str) and "*/" in comment: if isinstance(comment, str) and "*/" in comment:
# tell the user "no" so they don't hurt themselves by writing # tell the user "no" so they don't hurt themselves by writing
# garbage # garbage
raise DbtRuntimeError(f'query comment contains illegal value "*/": {comment}') raise RuntimeException(f'query comment contains illegal value "*/": {comment}')
self.query_comment = comment self.query_comment = comment
self.append = append self.append = append
@@ -90,7 +90,7 @@ class MacroQueryStringSetter:
def reset(self): def reset(self):
self.set("master", None) self.set("master", None)
def set(self, name: str, node: Optional[ResultNode]): def set(self, name: str, node: Optional[CompileResultNode]):
wrapped: Optional[NodeWrapper] = None wrapped: Optional[NodeWrapper] = None
if node is not None: if node is not None:
wrapped = NodeWrapper(node) wrapped = NodeWrapper(node)

View File

@@ -1,8 +1,9 @@
from collections.abc import Hashable from collections.abc import Hashable
from dataclasses import dataclass, field from dataclasses import dataclass
from typing import Optional, TypeVar, Any, Type, Dict, Iterator, Tuple, Set from typing import Optional, TypeVar, Any, Type, Dict, Union, Iterator, Tuple, Set
from dbt.contracts.graph.nodes import SourceDefinition, ManifestNode, ResultNode, ParsedNode from dbt.contracts.graph.compiled import CompiledNode
from dbt.contracts.graph.parsed import ParsedSourceDefinition, ParsedNode
from dbt.contracts.relation import ( from dbt.contracts.relation import (
RelationType, RelationType,
ComponentName, ComponentName,
@@ -11,11 +12,7 @@ from dbt.contracts.relation import (
Policy, Policy,
Path, Path,
) )
from dbt.exceptions import ( from dbt.exceptions import InternalException
ApproximateMatchError,
DbtInternalError,
MultipleDatabasesNotAllowedError,
)
from dbt.node_types import NodeType from dbt.node_types import NodeType
from dbt.utils import filter_null_values, deep_merge, classproperty from dbt.utils import filter_null_values, deep_merge, classproperty
@@ -30,10 +27,8 @@ class BaseRelation(FakeAPIObject, Hashable):
path: Path path: Path
type: Optional[RelationType] = None type: Optional[RelationType] = None
quote_character: str = '"' quote_character: str = '"'
# Python 3.11 requires that these use default_factory instead of simple default include_policy: Policy = Policy()
# ValueError: mutable default <class 'dbt.contracts.relation.Policy'> for field include_policy is not allowed: use default_factory quote_policy: Policy = Policy()
include_policy: Policy = field(default_factory=lambda: Policy())
quote_policy: Policy = field(default_factory=lambda: Policy())
dbt_created: bool = False dbt_created: bool = False
def _is_exactish_match(self, field: ComponentName, value: str) -> bool: def _is_exactish_match(self, field: ComponentName, value: str) -> bool:
@@ -44,9 +39,9 @@ class BaseRelation(FakeAPIObject, Hashable):
@classmethod @classmethod
def _get_field_named(cls, field_name): def _get_field_named(cls, field_name):
for f, _ in cls._get_fields(): for field, _ in cls._get_fields():
if f.name == field_name: if field.name == field_name:
return f return field
# this should be unreachable # this should be unreachable
raise ValueError(f"BaseRelation has no {field_name} field!") raise ValueError(f"BaseRelation has no {field_name} field!")
@@ -57,11 +52,11 @@ class BaseRelation(FakeAPIObject, Hashable):
@classmethod @classmethod
def get_default_quote_policy(cls) -> Policy: def get_default_quote_policy(cls) -> Policy:
return cls._get_field_named("quote_policy").default_factory() return cls._get_field_named("quote_policy").default
@classmethod @classmethod
def get_default_include_policy(cls) -> Policy: def get_default_include_policy(cls) -> Policy:
return cls._get_field_named("include_policy").default_factory() return cls._get_field_named("include_policy").default
def get(self, key, default=None): def get(self, key, default=None):
"""Override `.get` to return a metadata object so we don't break """Override `.get` to return a metadata object so we don't break
@@ -87,7 +82,7 @@ class BaseRelation(FakeAPIObject, Hashable):
if not search: if not search:
# nothing was passed in # nothing was passed in
raise dbt.exceptions.DbtRuntimeError( raise dbt.exceptions.RuntimeException(
"Tried to match relation, but no search path was passed!" "Tried to match relation, but no search path was passed!"
) )
@@ -104,7 +99,7 @@ class BaseRelation(FakeAPIObject, Hashable):
if approximate_match and not exact_match: if approximate_match and not exact_match:
target = self.create(database=database, schema=schema, identifier=identifier) target = self.create(database=database, schema=schema, identifier=identifier)
raise ApproximateMatchError(target, self) dbt.exceptions.approximate_relation_match(target, self)
return exact_match return exact_match
@@ -189,7 +184,7 @@ class BaseRelation(FakeAPIObject, Hashable):
) )
@classmethod @classmethod
def create_from_source(cls: Type[Self], source: SourceDefinition, **kwargs: Any) -> Self: def create_from_source(cls: Type[Self], source: ParsedSourceDefinition, **kwargs: Any) -> Self:
source_quoting = source.quoting.to_dict(omit_none=True) source_quoting = source.quoting.to_dict(omit_none=True)
source_quoting.pop("column", None) source_quoting.pop("column", None)
quote_policy = deep_merge( quote_policy = deep_merge(
@@ -214,7 +209,7 @@ class BaseRelation(FakeAPIObject, Hashable):
def create_ephemeral_from_node( def create_ephemeral_from_node(
cls: Type[Self], cls: Type[Self],
config: HasQuoting, config: HasQuoting,
node: ManifestNode, node: Union[ParsedNode, CompiledNode],
) -> Self: ) -> Self:
# Note that ephemeral models are based on the name. # Note that ephemeral models are based on the name.
identifier = cls.add_ephemeral_prefix(node.name) identifier = cls.add_ephemeral_prefix(node.name)
@@ -227,7 +222,7 @@ class BaseRelation(FakeAPIObject, Hashable):
def create_from_node( def create_from_node(
cls: Type[Self], cls: Type[Self],
config: HasQuoting, config: HasQuoting,
node: ManifestNode, node: Union[ParsedNode, CompiledNode],
quote_policy: Optional[Dict[str, bool]] = None, quote_policy: Optional[Dict[str, bool]] = None,
**kwargs: Any, **kwargs: Any,
) -> Self: ) -> Self:
@@ -248,20 +243,20 @@ class BaseRelation(FakeAPIObject, Hashable):
def create_from( def create_from(
cls: Type[Self], cls: Type[Self],
config: HasQuoting, config: HasQuoting,
node: ResultNode, node: Union[CompiledNode, ParsedNode, ParsedSourceDefinition],
**kwargs: Any, **kwargs: Any,
) -> Self: ) -> Self:
if node.resource_type == NodeType.Source: if node.resource_type == NodeType.Source:
if not isinstance(node, SourceDefinition): if not isinstance(node, ParsedSourceDefinition):
raise DbtInternalError( raise InternalException(
"type mismatch, expected SourceDefinition but got {}".format(type(node)) "type mismatch, expected ParsedSourceDefinition but got {}".format(type(node))
) )
return cls.create_from_source(node, **kwargs) return cls.create_from_source(node, **kwargs)
else: else:
# Can't use ManifestNode here because of parameterized generics if not isinstance(node, (ParsedNode, CompiledNode)):
if not isinstance(node, (ParsedNode)): raise InternalException(
raise DbtInternalError( "type mismatch, expected ParsedNode or CompiledNode but "
f"type mismatch, expected ManifestNode but got {type(node)}" "got {}".format(type(node))
) )
return cls.create_from_node(config, node, **kwargs) return cls.create_from_node(config, node, **kwargs)
@@ -358,7 +353,7 @@ class InformationSchema(BaseRelation):
def __post_init__(self): def __post_init__(self):
if not isinstance(self.information_schema_view, (type(None), str)): if not isinstance(self.information_schema_view, (type(None), str)):
raise dbt.exceptions.CompilationError( raise dbt.exceptions.CompilationException(
"Got an invalid name: {}".format(self.information_schema_view) "Got an invalid name: {}".format(self.information_schema_view)
) )
@@ -442,7 +437,7 @@ class SchemaSearchMap(Dict[InformationSchema, Set[Optional[str]]]):
if not allow_multiple_databases: if not allow_multiple_databases:
seen = {r.database.lower() for r in self if r.database} seen = {r.database.lower() for r in self if r.database}
if len(seen) > 1: if len(seen) > 1:
raise MultipleDatabasesNotAllowedError(seen) dbt.exceptions.raise_compiler_error(str(seen))
for information_schema_name, schema in self.search(): for information_schema_name, schema in self.search():
path = {"database": information_schema_name.database, "schema": schema} path = {"database": information_schema_name.database, "schema": schema}

View File

@@ -2,23 +2,26 @@ import threading
from copy import deepcopy from copy import deepcopy
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
from dbt.adapters.reference_keys import ( from dbt.adapters.reference_keys import _make_key, _ReferenceKey
_make_ref_key, import dbt.exceptions
_make_ref_key_msg, from dbt.events.functions import fire_event
_make_msg_from_ref_key, from dbt.events.types import (
_ReferenceKey, AddLink,
AddRelation,
DropCascade,
DropMissingRelation,
DropRelation,
DumpAfterAddGraph,
DumpAfterRenameSchema,
DumpBeforeAddGraph,
DumpBeforeRenameSchema,
RenameSchema,
TemporaryRelation,
UncachedRelation,
UpdateReference,
) )
from dbt.exceptions import (
DependentLinkNotCachedError,
NewNameAlreadyInCacheError,
NoneRelationFoundError,
ReferencedLinkNotCachedError,
TruncatedModelNameCausedCollisionError,
)
from dbt.events.functions import fire_event, fire_event_if
from dbt.events.types import CacheAction, CacheDumpGraph
import dbt.flags as flags
from dbt.utils import lowercase from dbt.utils import lowercase
from dbt.helper_types import Lazy
def dot_separated(key: _ReferenceKey) -> str: def dot_separated(key: _ReferenceKey) -> str:
@@ -78,7 +81,7 @@ class _CachedRelation:
:return _ReferenceKey: A key for this relation. :return _ReferenceKey: A key for this relation.
""" """
return _make_ref_key(self) return _make_key(self)
def add_reference(self, referrer: "_CachedRelation"): def add_reference(self, referrer: "_CachedRelation"):
"""Add a reference from referrer to self, indicating that if this node """Add a reference from referrer to self, indicating that if this node
@@ -141,7 +144,11 @@ class _CachedRelation:
:raises InternalError: If the new key already exists. :raises InternalError: If the new key already exists.
""" """
if new_key in self.referenced_by: if new_key in self.referenced_by:
raise NewNameAlreadyInCacheError(old_key, new_key) dbt.exceptions.raise_cache_inconsistent(
'in rename of "{}" -> "{}", new name is in the cache already'.format(
old_key, new_key
)
)
if old_key not in self.referenced_by: if old_key not in self.referenced_by:
return return
@@ -257,17 +264,21 @@ class RelationsCache:
if referenced is None: if referenced is None:
return return
if referenced is None: if referenced is None:
raise ReferencedLinkNotCachedError(referenced_key) dbt.exceptions.raise_cache_inconsistent(
"in add_link, referenced link key {} not in cache!".format(referenced_key)
)
dependent = self.relations.get(dependent_key) dependent = self.relations.get(dependent_key)
if dependent is None: if dependent is None:
raise DependentLinkNotCachedError(dependent_key) dbt.exceptions.raise_cache_inconsistent(
"in add_link, dependent link key {} not in cache!".format(dependent_key)
)
assert dependent is not None # we just raised! assert dependent is not None # we just raised!
referenced.add_reference(dependent) referenced.add_reference(dependent)
# This is called in plugins/postgres/dbt/adapters/postgres/impl.py # TODO: Is this dead code? I can't seem to find it grepping the codebase.
def add_link(self, referenced, dependent): def add_link(self, referenced, dependent):
"""Add a link between two relations to the database. If either relation """Add a link between two relations to the database. If either relation
does not exist, it will be added as an "external" relation. does not exist, it will be added as an "external" relation.
@@ -282,18 +293,13 @@ class RelationsCache:
:param BaseRelation dependent: The dependent model. :param BaseRelation dependent: The dependent model.
:raises InternalError: If either entry does not exist. :raises InternalError: If either entry does not exist.
""" """
ref_key = _make_ref_key(referenced) ref_key = _make_key(referenced)
dep_key = _make_ref_key(dependent) dep_key = _make_key(dependent)
if (ref_key.database, ref_key.schema) not in self: if (ref_key.database, ref_key.schema) not in self:
# if we have not cached the referenced schema at all, we must be # if we have not cached the referenced schema at all, we must be
# referring to a table outside our control. There's no need to make # referring to a table outside our control. There's no need to make
# a link - we will never drop the referenced relation during a run. # a link - we will never drop the referenced relation during a run.
fire_event( fire_event(UncachedRelation(dep_key=dep_key, ref_key=ref_key))
CacheAction(
ref_key=_make_msg_from_ref_key(ref_key),
ref_key_2=_make_msg_from_ref_key(dep_key),
)
)
return return
if ref_key not in self.relations: if ref_key not in self.relations:
# Insert a dummy "external" relation. # Insert a dummy "external" relation.
@@ -303,13 +309,7 @@ class RelationsCache:
# Insert a dummy "external" relation. # Insert a dummy "external" relation.
dependent = dependent.replace(type=referenced.External) dependent = dependent.replace(type=referenced.External)
self.add(dependent) self.add(dependent)
fire_event( fire_event(AddLink(dep_key=dep_key, ref_key=ref_key))
CacheAction(
action="add_link",
ref_key=_make_msg_from_ref_key(dep_key),
ref_key_2=_make_msg_from_ref_key(ref_key),
)
)
with self.lock: with self.lock:
self._add_link(ref_key, dep_key) self._add_link(ref_key, dep_key)
@@ -320,18 +320,12 @@ class RelationsCache:
:param BaseRelation relation: The underlying relation. :param BaseRelation relation: The underlying relation.
""" """
cached = _CachedRelation(relation) cached = _CachedRelation(relation)
fire_event_if( fire_event(AddRelation(relation=_make_key(cached)))
flags.LOG_CACHE_EVENTS, fire_event(DumpBeforeAddGraph(dump=Lazy.defer(lambda: self.dump_graph())))
lambda: CacheDumpGraph(before_after="before", action="adding", dump=self.dump_graph()),
)
fire_event(CacheAction(action="add_relation", ref_key=_make_ref_key_msg(cached)))
with self.lock: with self.lock:
self._setdefault(cached) self._setdefault(cached)
fire_event_if( fire_event(DumpAfterAddGraph(dump=Lazy.defer(lambda: self.dump_graph())))
flags.LOG_CACHE_EVENTS,
lambda: CacheDumpGraph(before_after="after", action="adding", dump=self.dump_graph()),
)
def _remove_refs(self, keys): def _remove_refs(self, keys):
"""Removes all references to all entries in keys. This does not """Removes all references to all entries in keys. This does not
@@ -346,6 +340,19 @@ class RelationsCache:
for cached in self.relations.values(): for cached in self.relations.values():
cached.release_references(keys) cached.release_references(keys)
def _drop_cascade_relation(self, dropped_key):
"""Drop the given relation and cascade it appropriately to all
dependent relations.
:param _CachedRelation dropped: An existing _CachedRelation to drop.
"""
if dropped_key not in self.relations:
fire_event(DropMissingRelation(relation=dropped_key))
return
consequences = self.relations[dropped_key].collect_consequences()
fire_event(DropCascade(dropped=dropped_key, consequences=consequences))
self._remove_refs(consequences)
def drop(self, relation): def drop(self, relation):
"""Drop the named relation and cascade it appropriately to all """Drop the named relation and cascade it appropriately to all
dependent relations. dependent relations.
@@ -357,22 +364,10 @@ class RelationsCache:
:param str schema: The schema of the relation to drop. :param str schema: The schema of the relation to drop.
:param str identifier: The identifier of the relation to drop. :param str identifier: The identifier of the relation to drop.
""" """
dropped_key = _make_ref_key(relation) dropped_key = _make_key(relation)
dropped_key_msg = _make_ref_key_msg(relation) fire_event(DropRelation(dropped=dropped_key))
fire_event(CacheAction(action="drop_relation", ref_key=dropped_key_msg))
with self.lock: with self.lock:
if dropped_key not in self.relations: self._drop_cascade_relation(dropped_key)
fire_event(CacheAction(action="drop_missing_relation", ref_key=dropped_key_msg))
return
consequences = self.relations[dropped_key].collect_consequences()
# convert from a list of _ReferenceKeys to a list of ReferenceKeyMsgs
consequence_msgs = [_make_msg_from_ref_key(key) for key in consequences]
fire_event(
CacheAction(
action="drop_cascade", ref_key=dropped_key_msg, ref_list=consequence_msgs
)
)
self._remove_refs(consequences)
def _rename_relation(self, old_key, new_relation): def _rename_relation(self, old_key, new_relation):
"""Rename a relation named old_key to new_key, updating references. """Rename a relation named old_key to new_key, updating references.
@@ -388,20 +383,14 @@ class RelationsCache:
relation = self.relations.pop(old_key) relation = self.relations.pop(old_key)
new_key = new_relation.key() new_key = new_relation.key()
# relation has to rename its innards, so it needs the _CachedRelation. # relaton has to rename its innards, so it needs the _CachedRelation.
relation.rename(new_relation) relation.rename(new_relation)
# update all the relations that refer to it # update all the relations that refer to it
for cached in self.relations.values(): for cached in self.relations.values():
if cached.is_referenced_by(old_key): if cached.is_referenced_by(old_key):
fire_event( fire_event(
CacheAction( UpdateReference(old_key=old_key, new_key=new_key, cached_key=cached.key())
action="update_reference",
ref_key=_make_ref_key_msg(old_key),
ref_key_2=_make_ref_key_msg(new_key),
ref_key_3=_make_ref_key_msg(cached.key()),
) )
)
cached.rename_key(old_key, new_key) cached.rename_key(old_key, new_key)
self.relations[new_key] = relation self.relations[new_key] = relation
@@ -424,14 +413,14 @@ class RelationsCache:
:raises InternalError: If the new key is already present. :raises InternalError: If the new key is already present.
""" """
if new_key in self.relations: if new_key in self.relations:
# Tell user when collision caused by model names truncated during dbt.exceptions.raise_cache_inconsistent(
# materialization. "in rename, new key {} already in cache: {}".format(
raise TruncatedModelNameCausedCollisionError(new_key, self.relations) new_key, list(self.relations.keys())
)
)
if old_key not in self.relations: if old_key not in self.relations:
fire_event( fire_event(TemporaryRelation(key=old_key))
CacheAction(action="temporary_relation", ref_key=_make_msg_from_ref_key(old_key))
)
return False return False
return True return True
@@ -447,20 +436,11 @@ class RelationsCache:
:param BaseRelation new: The new relation name information. :param BaseRelation new: The new relation name information.
:raises InternalError: If the new key is already present. :raises InternalError: If the new key is already present.
""" """
old_key = _make_ref_key(old) old_key = _make_key(old)
new_key = _make_ref_key(new) new_key = _make_key(new)
fire_event( fire_event(RenameSchema(old_key=old_key, new_key=new_key))
CacheAction(
action="rename_relation",
ref_key=_make_msg_from_ref_key(old_key),
ref_key_2=_make_msg_from_ref_key(new),
)
)
fire_event_if( fire_event(DumpBeforeRenameSchema(dump=Lazy.defer(lambda: self.dump_graph())))
flags.LOG_CACHE_EVENTS,
lambda: CacheDumpGraph(before_after="before", action="rename", dump=self.dump_graph()),
)
with self.lock: with self.lock:
if self._check_rename_constraints(old_key, new_key): if self._check_rename_constraints(old_key, new_key):
@@ -468,10 +448,7 @@ class RelationsCache:
else: else:
self._setdefault(_CachedRelation(new)) self._setdefault(_CachedRelation(new))
fire_event_if( fire_event(DumpAfterRenameSchema(dump=Lazy.defer(lambda: self.dump_graph())))
flags.LOG_CACHE_EVENTS,
lambda: CacheDumpGraph(before_after="after", action="rename", dump=self.dump_graph()),
)
def get_relations(self, database: Optional[str], schema: Optional[str]) -> List[Any]: def get_relations(self, database: Optional[str], schema: Optional[str]) -> List[Any]:
"""Case-insensitively yield all relations matching the given schema. """Case-insensitively yield all relations matching the given schema.
@@ -490,7 +467,9 @@ class RelationsCache:
] ]
if None in results: if None in results:
raise NoneRelationFoundError() dbt.exceptions.raise_cache_inconsistent(
"in get_relations, a None relation was found in the cache!"
)
return results return results
def clear(self): def clear(self):
@@ -517,6 +496,6 @@ class RelationsCache:
""" """
for relation in to_remove: for relation in to_remove:
# it may have been cascaded out already # it may have been cascaded out already
drop_key = _make_ref_key(relation) drop_key = _make_key(relation)
if drop_key in self.relations: if drop_key in self.relations:
self.drop(drop_key) self.drop(drop_key)

View File

@@ -1,18 +1,23 @@
import threading import threading
import traceback
from contextlib import contextmanager
from importlib import import_module
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Type from importlib import import_module
from typing import Type, Dict, Any, List, Optional, Set
from dbt.adapters.base.plugin import AdapterPlugin from dbt.exceptions import RuntimeException, InternalException
from dbt.adapters.protocol import AdapterConfig, AdapterProtocol, RelationProtocol from dbt.include.global_project import (
from dbt.contracts.connection import AdapterRequiredConfig, Credentials PACKAGE_PATH as GLOBAL_PROJECT_PATH,
PROJECT_NAME as GLOBAL_PROJECT_NAME,
)
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import AdapterImportError, PluginLoadError from dbt.events.types import AdapterImportError, PluginLoadError
from dbt.exceptions import DbtInternalError, DbtRuntimeError from dbt.contracts.connection import Credentials, AdapterRequiredConfig
from dbt.include.global_project import PACKAGE_PATH as GLOBAL_PROJECT_PATH from dbt.adapters.protocol import (
from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME AdapterProtocol,
AdapterConfig,
RelationProtocol,
)
from dbt.adapters.base.plugin import AdapterPlugin
Adapter = AdapterProtocol Adapter = AdapterProtocol
@@ -34,7 +39,7 @@ class AdapterContainer:
names = ", ".join(self.plugins.keys()) names = ", ".join(self.plugins.keys())
message = f"Invalid adapter type {name}! Must be one of {names}" message = f"Invalid adapter type {name}! Must be one of {names}"
raise DbtRuntimeError(message) raise RuntimeException(message)
def get_adapter_class_by_name(self, name: str) -> Type[Adapter]: def get_adapter_class_by_name(self, name: str) -> Type[Adapter]:
plugin = self.get_plugin_by_name(name) plugin = self.get_plugin_by_name(name)
@@ -59,18 +64,18 @@ class AdapterContainer:
# if we failed to import the target module in particular, inform # if we failed to import the target module in particular, inform
# the user about it via a runtime error # the user about it via a runtime error
if exc.name == "dbt.adapters." + name: if exc.name == "dbt.adapters." + name:
fire_event(AdapterImportError(exc=str(exc))) fire_event(AdapterImportError(exc=exc))
raise DbtRuntimeError(f"Could not find adapter type {name}!") raise RuntimeException(f"Could not find adapter type {name}!")
# otherwise, the error had to have come from some underlying # otherwise, the error had to have come from some underlying
# library. Log the stack trace. # library. Log the stack trace.
fire_event(PluginLoadError(exc_info=traceback.format_exc())) fire_event(PluginLoadError())
raise raise
plugin: AdapterPlugin = mod.Plugin plugin: AdapterPlugin = mod.Plugin
plugin_type = plugin.adapter.type() plugin_type = plugin.adapter.type()
if plugin_type != name: if plugin_type != name:
raise DbtRuntimeError( raise RuntimeException(
f"Expected to find adapter with type named {name}, got " f"Expected to find adapter with type named {name}, got "
f"adapter with type {plugin_type}" f"adapter with type {plugin_type}"
) )
@@ -132,9 +137,11 @@ class AdapterContainer:
try: try:
plugin = self.plugins[plugin_name] plugin = self.plugins[plugin_name]
except KeyError: except KeyError:
raise DbtInternalError(f"No plugin found for {plugin_name}") from None raise InternalException(f"No plugin found for {plugin_name}") from None
plugins.append(plugin) plugins.append(plugin)
seen.add(plugin_name) seen.add(plugin_name)
if plugin.dependencies is None:
continue
for dep in plugin.dependencies: for dep in plugin.dependencies:
if dep not in seen: if dep not in seen:
plugin_names.append(dep) plugin_names.append(dep)
@@ -151,7 +158,7 @@ class AdapterContainer:
try: try:
path = self.packages[package_name] path = self.packages[package_name]
except KeyError: except KeyError:
raise DbtInternalError(f"No internal package listing found for {package_name}") raise InternalException(f"No internal package listing found for {package_name}")
paths.append(path) paths.append(path)
return paths return paths
@@ -212,12 +219,3 @@ def get_adapter_package_names(name: Optional[str]) -> List[str]:
def get_adapter_type_names(name: Optional[str]) -> List[str]: def get_adapter_type_names(name: Optional[str]) -> List[str]:
return FACTORY.get_adapter_type_names(name) return FACTORY.get_adapter_type_names(name)
@contextmanager
def adapter_management():
reset_adapters()
try:
yield
finally:
cleanup_connections()

View File

@@ -7,7 +7,9 @@ from typing import (
List, List,
Generic, Generic,
TypeVar, TypeVar,
ClassVar,
Tuple, Tuple,
Union,
Dict, Dict,
Any, Any,
) )
@@ -16,7 +18,8 @@ from typing_extensions import Protocol
import agate import agate
from dbt.contracts.connection import Connection, AdapterRequiredConfig, AdapterResponse from dbt.contracts.connection import Connection, AdapterRequiredConfig, AdapterResponse
from dbt.contracts.graph.nodes import ResultNode, ManifestNode from dbt.contracts.graph.compiled import CompiledNode, ManifestNode, NonSourceCompiledNode
from dbt.contracts.graph.parsed import ParsedNode, ParsedSourceDefinition
from dbt.contracts.graph.model_config import BaseConfig from dbt.contracts.graph.model_config import BaseConfig
from dbt.contracts.graph.manifest import Manifest from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.relation import Policy, HasQuoting from dbt.contracts.relation import Policy, HasQuoting
@@ -46,7 +49,11 @@ class RelationProtocol(Protocol):
... ...
@classmethod @classmethod
def create_from(cls: Type[Self], config: HasQuoting, node: ResultNode) -> Self: def create_from(
cls: Type[Self],
config: HasQuoting,
node: Union[CompiledNode, ParsedNode, ParsedSourceDefinition],
) -> Self:
... ...
@@ -59,7 +66,7 @@ class CompilerProtocol(Protocol):
node: ManifestNode, node: ManifestNode,
manifest: Manifest, manifest: Manifest,
extra_context: Optional[Dict[str, Any]] = None, extra_context: Optional[Dict[str, Any]] = None,
) -> ManifestNode: ) -> NonSourceCompiledNode:
... ...
@@ -81,13 +88,10 @@ class AdapterProtocol( # type: ignore[misc]
Compiler_T, Compiler_T,
], ],
): ):
# N.B. Technically these are ClassVars, but mypy doesn't support putting type vars in a AdapterSpecificConfigs: ClassVar[Type[AdapterConfig_T]]
# ClassVar due to the restrictiveness of PEP-526 Column: ClassVar[Type[Column_T]]
# See: https://github.com/python/mypy/issues/5144 Relation: ClassVar[Type[Relation_T]]
AdapterSpecificConfigs: Type[AdapterConfig_T] ConnectionManager: ClassVar[Type[ConnectionManager_T]]
Column: Type[Column_T]
Relation: Type[Relation_T]
ConnectionManager: Type[ConnectionManager_T]
connections: ConnectionManager_T connections: ConnectionManager_T
def __init__(self, config: AdapterRequiredConfig): def __init__(self, config: AdapterRequiredConfig):

View File

@@ -1,8 +1,7 @@
# this module exists to resolve circular imports with the events module # this module exists to resolve circular imports with the events module
from collections import namedtuple from collections import namedtuple
from typing import Any, Optional from typing import Optional
from dbt.events.proto_types import ReferenceKeyMsg
_ReferenceKey = namedtuple("_ReferenceKey", "database schema identifier") _ReferenceKey = namedtuple("_ReferenceKey", "database schema identifier")
@@ -15,12 +14,7 @@ def lowercase(value: Optional[str]) -> Optional[str]:
return value.lower() return value.lower()
# For backwards compatibility. New code should use _make_ref_key def _make_key(relation) -> _ReferenceKey:
def _make_key(relation: Any) -> _ReferenceKey:
return _make_ref_key(relation)
def _make_ref_key(relation: Any) -> _ReferenceKey:
"""Make _ReferenceKeys with lowercase values for the cache so we don't have """Make _ReferenceKeys with lowercase values for the cache so we don't have
to keep track of quoting to keep track of quoting
""" """
@@ -28,13 +22,3 @@ def _make_ref_key(relation: Any) -> _ReferenceKey:
return _ReferenceKey( return _ReferenceKey(
lowercase(relation.database), lowercase(relation.schema), lowercase(relation.identifier) lowercase(relation.database), lowercase(relation.schema), lowercase(relation.identifier)
) )
def _make_ref_key_msg(relation: Any):
return _make_msg_from_ref_key(_make_ref_key(relation))
def _make_msg_from_ref_key(ref_key: _ReferenceKey) -> ReferenceKeyMsg:
return ReferenceKeyMsg(
database=ref_key.database, schema=ref_key.schema, identifier=ref_key.identifier
)

View File

@@ -10,8 +10,6 @@ from dbt.adapters.base import BaseConnectionManager
from dbt.contracts.connection import Connection, ConnectionState, AdapterResponse from dbt.contracts.connection import Connection, ConnectionState, AdapterResponse
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import ConnectionUsed, SQLQuery, SQLCommit, SQLQueryStatus from dbt.events.types import ConnectionUsed, SQLQuery, SQLCommit, SQLQueryStatus
from dbt.events.contextvars import get_node_info
from dbt.utils import cast_to_str
class SQLConnectionManager(BaseConnectionManager): class SQLConnectionManager(BaseConnectionManager):
@@ -27,7 +25,9 @@ class SQLConnectionManager(BaseConnectionManager):
@abc.abstractmethod @abc.abstractmethod
def cancel(self, connection: Connection): def cancel(self, connection: Connection):
"""Cancel the given connection.""" """Cancel the given connection."""
raise dbt.exceptions.NotImplementedError("`cancel` is not implemented for this adapter!") raise dbt.exceptions.NotImplementedException(
"`cancel` is not implemented for this adapter!"
)
def cancel_open(self) -> List[str]: def cancel_open(self) -> List[str]:
names = [] names = []
@@ -55,13 +55,7 @@ class SQLConnectionManager(BaseConnectionManager):
connection = self.get_thread_connection() connection = self.get_thread_connection()
if auto_begin and connection.transaction_open is False: if auto_begin and connection.transaction_open is False:
self.begin() self.begin()
fire_event( fire_event(ConnectionUsed(conn_type=self.TYPE, conn_name=connection.name))
ConnectionUsed(
conn_type=self.TYPE,
conn_name=cast_to_str(connection.name),
node_info=get_node_info(),
)
)
with self.exception_handler(sql): with self.exception_handler(sql):
if abridge_sql_log: if abridge_sql_log:
@@ -69,11 +63,7 @@ class SQLConnectionManager(BaseConnectionManager):
else: else:
log_sql = sql log_sql = sql
fire_event( fire_event(SQLQuery(conn_name=connection.name, sql=log_sql))
SQLQuery(
conn_name=cast_to_str(connection.name), sql=log_sql, node_info=get_node_info()
)
)
pre = time.time() pre = time.time()
cursor = connection.handle.cursor() cursor = connection.handle.cursor()
@@ -81,19 +71,16 @@ class SQLConnectionManager(BaseConnectionManager):
fire_event( fire_event(
SQLQueryStatus( SQLQueryStatus(
status=str(self.get_response(cursor)), status=str(self.get_response(cursor)), elapsed=round((time.time() - pre), 2)
elapsed=round((time.time() - pre)),
node_info=get_node_info(),
) )
) )
return connection, cursor return connection, cursor
@classmethod @abc.abstractclassmethod
@abc.abstractmethod
def get_response(cls, cursor: Any) -> AdapterResponse: def get_response(cls, cursor: Any) -> AdapterResponse:
"""Get the status of the cursor.""" """Get the status of the cursor."""
raise dbt.exceptions.NotImplementedError( raise dbt.exceptions.NotImplementedException(
"`get_response` is not implemented for this adapter!" "`get_response` is not implemented for this adapter!"
) )
@@ -149,7 +136,7 @@ class SQLConnectionManager(BaseConnectionManager):
def begin(self): def begin(self):
connection = self.get_thread_connection() connection = self.get_thread_connection()
if connection.transaction_open is True: if connection.transaction_open is True:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
'Tried to begin a new transaction on connection "{}", but ' 'Tried to begin a new transaction on connection "{}", but '
"it already had one open!".format(connection.name) "it already had one open!".format(connection.name)
) )
@@ -162,12 +149,12 @@ class SQLConnectionManager(BaseConnectionManager):
def commit(self): def commit(self):
connection = self.get_thread_connection() connection = self.get_thread_connection()
if connection.transaction_open is False: if connection.transaction_open is False:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
'Tried to commit transaction on connection "{}", but ' 'Tried to commit transaction on connection "{}", but '
"it does not have one open!".format(connection.name) "it does not have one open!".format(connection.name)
) )
fire_event(SQLCommit(conn_name=connection.name, node_info=get_node_info())) fire_event(SQLCommit(conn_name=connection.name))
self.add_commit_query() self.add_commit_query()
connection.transaction_open = False connection.transaction_open = False

View File

@@ -1,10 +1,11 @@
import agate import agate
from typing import Any, Optional, Tuple, Type, List from typing import Any, Optional, Tuple, Type, List
import dbt.clients.agate_helper
from dbt.contracts.connection import Connection from dbt.contracts.connection import Connection
from dbt.exceptions import RelationTypeNullError import dbt.exceptions
from dbt.adapters.base import BaseAdapter, available from dbt.adapters.base import BaseAdapter, available
from dbt.adapters.cache import _make_ref_key_msg from dbt.adapters.cache import _make_key
from dbt.adapters.sql import SQLConnectionManager from dbt.adapters.sql import SQLConnectionManager
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import ColTypeChange, SchemaCreation, SchemaDrop from dbt.events.types import ColTypeChange, SchemaCreation, SchemaDrop
@@ -109,7 +110,7 @@ class SQLAdapter(BaseAdapter):
ColTypeChange( ColTypeChange(
orig_type=target_column.data_type, orig_type=target_column.data_type,
new_type=new_type, new_type=new_type,
table=_make_ref_key_msg(current), table=_make_key(current),
) )
) )
@@ -131,7 +132,9 @@ class SQLAdapter(BaseAdapter):
def drop_relation(self, relation): def drop_relation(self, relation):
if relation.type is None: if relation.type is None:
raise RelationTypeNullError(relation) dbt.exceptions.raise_compiler_error(
"Tried to drop relation {}, but its type is null.".format(relation)
)
self.cache_dropped(relation) self.cache_dropped(relation)
self.execute_macro(DROP_RELATION_MACRO_NAME, kwargs={"relation": relation}) self.execute_macro(DROP_RELATION_MACRO_NAME, kwargs={"relation": relation})
@@ -152,7 +155,7 @@ class SQLAdapter(BaseAdapter):
def create_schema(self, relation: BaseRelation) -> None: def create_schema(self, relation: BaseRelation) -> None:
relation = relation.without_identifier() relation = relation.without_identifier()
fire_event(SchemaCreation(relation=_make_ref_key_msg(relation))) fire_event(SchemaCreation(relation=_make_key(relation)))
kwargs = { kwargs = {
"relation": relation, "relation": relation,
} }
@@ -163,12 +166,11 @@ class SQLAdapter(BaseAdapter):
def drop_schema(self, relation: BaseRelation) -> None: def drop_schema(self, relation: BaseRelation) -> None:
relation = relation.without_identifier() relation = relation.without_identifier()
fire_event(SchemaDrop(relation=_make_ref_key_msg(relation))) fire_event(SchemaDrop(relation=_make_key(relation)))
kwargs = { kwargs = {
"relation": relation, "relation": relation,
} }
self.execute_macro(DROP_SCHEMA_MACRO_NAME, kwargs=kwargs) self.execute_macro(DROP_SCHEMA_MACRO_NAME, kwargs=kwargs)
self.commit_if_has_connection()
# we can update the cache here # we can update the cache here
self.cache.drop_schema(relation.database, relation.schema) self.cache.drop_schema(relation.database, relation.schema)

View File

@@ -1 +0,0 @@
TODO

View File

@@ -1,44 +0,0 @@
# TODO Move this to /core/dbt/flags.py when we're ready to break things
import os
from dataclasses import dataclass
from multiprocessing import get_context
from pprint import pformat as pf
from click import get_current_context
if os.name != "nt":
# https://bugs.python.org/issue41567
import multiprocessing.popen_spawn_posix # type: ignore # noqa: F401
@dataclass(frozen=True)
class Flags:
def __init__(self, ctx=None) -> None:
if ctx is None:
ctx = get_current_context()
def assign_params(ctx):
"""Recursively adds all click params to flag object"""
for param_name, param_value in ctx.params.items():
# N.B. You have to use the base MRO method (object.__setattr__) to set attributes
# when using frozen dataclasses.
# https://docs.python.org/3/library/dataclasses.html#frozen-instances
if hasattr(self, param_name):
raise Exception(f"Duplicate flag names found in click command: {param_name}")
object.__setattr__(self, param_name.upper(), param_value)
if ctx.parent:
assign_params(ctx.parent)
assign_params(ctx)
# Hard coded flags
object.__setattr__(self, "WHICH", ctx.info_name)
object.__setattr__(self, "MP_CONTEXT", get_context("spawn"))
# Support console DO NOT TRACK initiave
if os.getenv("DO_NOT_TRACK", "").lower() in (1, "t", "true", "y", "yes"):
object.__setattr__(self, "ANONYMOUS_USAGE_STATS", False)
def __str__(self) -> str:
return str(pf(self.__dict__))

View File

@@ -1,412 +0,0 @@
import inspect # This is temporary for RAT-ing
from copy import copy
from pprint import pformat as pf # This is temporary for RAT-ing
import click
from dbt.adapters.factory import adapter_management
from dbt.cli import params as p
from dbt.cli.flags import Flags
from dbt.profiler import profiler
def cli_runner():
# Alias "list" to "ls"
ls = copy(cli.commands["list"])
ls.hidden = True
cli.add_command(ls, "ls")
# Run the cli
cli()
# dbt
@click.group(
context_settings={"help_option_names": ["-h", "--help"]},
invoke_without_command=True,
no_args_is_help=True,
epilog="Specify one of these sub-commands and you can find more help from there.",
)
@click.pass_context
@p.anonymous_usage_stats
@p.cache_selected_only
@p.debug
@p.enable_legacy_logger
@p.fail_fast
@p.log_cache_events
@p.log_format
@p.macro_debugging
@p.partial_parse
@p.print
@p.printer_width
@p.quiet
@p.record_timing_info
@p.static_parser
@p.use_colors
@p.use_experimental_parser
@p.version
@p.version_check
@p.warn_error
@p.warn_error_options
@p.write_json
def cli(ctx, **kwargs):
"""An ELT tool for managing your SQL transformations and data models.
For more documentation on these commands, visit: docs.getdbt.com
"""
incomplete_flags = Flags()
# Profiling
if incomplete_flags.RECORD_TIMING_INFO:
ctx.with_resource(profiler(enable=True, outfile=incomplete_flags.RECORD_TIMING_INFO))
# Adapter management
ctx.with_resource(adapter_management())
# Version info
if incomplete_flags.VERSION:
click.echo(f"`version` called\n ctx.params: {pf(ctx.params)}")
return
else:
del ctx.params["version"]
# dbt build
@cli.command("build")
@click.pass_context
@p.defer
@p.exclude
@p.fail_fast
@p.full_refresh
@p.indirect_selection
@p.log_path
@p.models
@p.profile
@p.profiles_dir
@p.project_dir
@p.selector
@p.show
@p.state
@p.store_failures
@p.target
@p.target_path
@p.threads
@p.vars
@p.version_check
def build(ctx, **kwargs):
"""Run all Seeds, Models, Snapshots, and tests in DAG order"""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt clean
@cli.command("clean")
@click.pass_context
@p.profile
@p.profiles_dir
@p.project_dir
@p.target
@p.vars
def clean(ctx, **kwargs):
"""Delete all folders in the clean-targets list (usually the dbt_packages and target directories.)"""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt docs
@cli.group()
@click.pass_context
def docs(ctx, **kwargs):
"""Generate or serve the documentation website for your project"""
# dbt docs generate
@docs.command("generate")
@click.pass_context
@p.compile_docs
@p.defer
@p.exclude
@p.log_path
@p.models
@p.profile
@p.profiles_dir
@p.project_dir
@p.selector
@p.state
@p.target
@p.target_path
@p.threads
@p.vars
@p.version_check
def docs_generate(ctx, **kwargs):
"""Generate the documentation website for your project"""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt docs serve
@docs.command("serve")
@click.pass_context
@p.browser
@p.port
@p.profile
@p.profiles_dir
@p.project_dir
@p.target
@p.vars
def docs_serve(ctx, **kwargs):
"""Serve the documentation website for your project"""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt compile
@cli.command("compile")
@click.pass_context
@p.defer
@p.exclude
@p.full_refresh
@p.log_path
@p.models
@p.parse_only
@p.profile
@p.profiles_dir
@p.project_dir
@p.selector
@p.state
@p.target
@p.target_path
@p.threads
@p.vars
@p.version_check
def compile(ctx, **kwargs):
"""Generates executable SQL from source, model, test, and analysis files. Compiled SQL files are written to the target/ directory."""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt debug
@cli.command("debug")
@click.pass_context
@p.config_dir
@p.profile
@p.profiles_dir
@p.project_dir
@p.target
@p.vars
@p.version_check
def debug(ctx, **kwargs):
"""Show some helpful information about dbt for debugging. Not to be confused with the --debug option which increases verbosity."""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt deps
@cli.command("deps")
@click.pass_context
@p.profile
@p.profiles_dir
@p.project_dir
@p.target
@p.vars
def deps(ctx, **kwargs):
"""Pull the most recent version of the dependencies listed in packages.yml"""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt init
@cli.command("init")
@click.pass_context
@p.profile
@p.profiles_dir
@p.project_dir
@p.skip_profile_setup
@p.target
@p.vars
def init(ctx, **kwargs):
"""Initialize a new DBT project."""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt list
@cli.command("list")
@click.pass_context
@p.exclude
@p.indirect_selection
@p.models
@p.output
@p.output_keys
@p.profile
@p.profiles_dir
@p.project_dir
@p.resource_type
@p.selector
@p.state
@p.target
@p.vars
def list(ctx, **kwargs):
"""List the resources in your project"""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt parse
@cli.command("parse")
@click.pass_context
@p.compile_parse
@p.log_path
@p.profile
@p.profiles_dir
@p.project_dir
@p.target
@p.target_path
@p.threads
@p.vars
@p.version_check
@p.write_manifest
def parse(ctx, **kwargs):
"""Parses the project and provides information on performance"""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt run
@cli.command("run")
@click.pass_context
@p.defer
@p.exclude
@p.fail_fast
@p.full_refresh
@p.log_path
@p.models
@p.profile
@p.profiles_dir
@p.project_dir
@p.selector
@p.state
@p.target
@p.target_path
@p.threads
@p.vars
@p.version_check
def run(ctx, **kwargs):
"""Compile SQL and execute against the current target database."""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt run operation
@cli.command("run-operation")
@click.pass_context
@p.args
@p.profile
@p.profiles_dir
@p.project_dir
@p.target
@p.vars
def run_operation(ctx, **kwargs):
"""Run the named macro with any supplied arguments."""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt seed
@cli.command("seed")
@click.pass_context
@p.exclude
@p.full_refresh
@p.log_path
@p.models
@p.profile
@p.profiles_dir
@p.project_dir
@p.selector
@p.show
@p.state
@p.target
@p.target_path
@p.threads
@p.vars
@p.version_check
def seed(ctx, **kwargs):
"""Load data from csv files into your data warehouse."""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt snapshot
@cli.command("snapshot")
@click.pass_context
@p.defer
@p.exclude
@p.models
@p.profile
@p.profiles_dir
@p.project_dir
@p.selector
@p.state
@p.target
@p.threads
@p.vars
def snapshot(ctx, **kwargs):
"""Execute snapshots defined in your project"""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt source
@cli.group()
@click.pass_context
def source(ctx, **kwargs):
"""Manage your project's sources"""
# dbt source freshness
@source.command("freshness")
@click.pass_context
@p.exclude
@p.models
@p.output_path # TODO: Is this ok to re-use? We have three different output params, how much can we consolidate?
@p.profile
@p.profiles_dir
@p.project_dir
@p.selector
@p.state
@p.target
@p.threads
@p.vars
def freshness(ctx, **kwargs):
"""Snapshots the current freshness of the project's sources"""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# dbt test
@cli.command("test")
@click.pass_context
@p.defer
@p.exclude
@p.fail_fast
@p.indirect_selection
@p.log_path
@p.models
@p.profile
@p.profiles_dir
@p.project_dir
@p.selector
@p.state
@p.store_failures
@p.target
@p.target_path
@p.threads
@p.vars
@p.version_check
def test(ctx, **kwargs):
"""Runs tests on data in deployed models. Run this after `dbt run`"""
flags = Flags()
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
# Support running as a module
if __name__ == "__main__":
cli_runner()

View File

@@ -1,48 +0,0 @@
from click import ParamType
import yaml
from dbt.helper_types import WarnErrorOptions
class YAML(ParamType):
"""The Click YAML type. Converts YAML strings into objects."""
name = "YAML"
def convert(self, value, param, ctx):
# assume non-string values are a problem
if not isinstance(value, str):
self.fail(f"Cannot load YAML from type {type(value)}", param, ctx)
try:
return yaml.load(value, Loader=yaml.Loader)
except yaml.parser.ParserError:
self.fail(f"String '{value}' is not valid YAML", param, ctx)
class WarnErrorOptionsType(YAML):
"""The Click WarnErrorOptions type. Converts YAML strings into objects."""
name = "WarnErrorOptionsType"
def convert(self, value, param, ctx):
include_exclude = super().convert(value, param, ctx)
return WarnErrorOptions(
include=include_exclude.get("include", []), exclude=include_exclude.get("exclude", [])
)
class Truthy(ParamType):
"""The Click Truthy type. Converts strings into a "truthy" type"""
name = "TRUTHY"
def convert(self, value, param, ctx):
# assume non-string / non-None values are a problem
if not isinstance(value, (str, None)):
self.fail(f"Cannot load TRUTHY from type {type(value)}", param, ctx)
if value is None or value.lower() in ("0", "false", "f"):
return None
else:
return value

View File

@@ -1,389 +0,0 @@
from pathlib import Path, PurePath
import click
from dbt.cli.option_types import YAML, WarnErrorOptionsType
from dbt.cli.resolvers import default_project_dir, default_profiles_dir
# TODO: The name (reflected in flags) is a correction!
# The original name was `SEND_ANONYMOUS_USAGE_STATS` and used an env var called "DBT_SEND_ANONYMOUS_USAGE_STATS"
# Both of which break existing naming conventions (doesn't match param flag).
# This will need to be fixed before use in the main codebase and communicated as a change to the community!
anonymous_usage_stats = click.option(
"--anonymous-usage-stats/--no-anonymous-usage-stats",
envvar="DBT_ANONYMOUS_USAGE_STATS",
help="Send anonymous usage stats to dbt Labs.",
default=True,
)
args = click.option(
"--args",
envvar=None,
help="Supply arguments to the macro. This dictionary will be mapped to the keyword arguments defined in the selected macro. This argument should be a YAML string, eg. '{my_variable: my_value}'",
type=YAML(),
)
browser = click.option(
"--browser/--no-browser",
envvar=None,
help="Wether or not to open a local web browser after starting the server",
default=True,
)
cache_selected_only = click.option(
"--cache-selected-only/--no-cache-selected-only",
envvar="DBT_CACHE_SELECTED_ONLY",
help="Pre cache database objects relevant to selected resource only.",
)
compile_docs = click.option(
"--compile/--no-compile",
envvar=None,
help="Wether or not to run 'dbt compile' as part of docs generation",
default=True,
)
compile_parse = click.option(
"--compile/--no-compile",
envvar=None,
help="TODO: No help text currently available",
default=True,
)
config_dir = click.option(
"--config-dir",
envvar=None,
help="If specified, DBT will show path information for this project",
type=click.STRING,
)
debug = click.option(
"--debug/--no-debug",
"-d/ ",
envvar="DBT_DEBUG",
help="Display debug logging during dbt execution. Useful for debugging and making bug reports.",
)
# TODO: The env var and name (reflected in flags) are corrections!
# The original name was `DEFER_MODE` and used an env var called "DBT_DEFER_TO_STATE"
# Both of which break existing naming conventions.
# This will need to be fixed before use in the main codebase and communicated as a change to the community!
defer = click.option(
"--defer/--no-defer",
envvar="DBT_DEFER",
help="If set, defer to the state variable for resolving unselected nodes.",
)
enable_legacy_logger = click.option(
"--enable-legacy-logger/--no-enable-legacy-logger",
envvar="DBT_ENABLE_LEGACY_LOGGER",
hidden=True,
)
exclude = click.option("--exclude", envvar=None, help="Specify the nodes to exclude.")
fail_fast = click.option(
"--fail-fast/--no-fail-fast",
"-x/ ",
envvar="DBT_FAIL_FAST",
help="Stop execution on first failure.",
)
full_refresh = click.option(
"--full-refresh",
"-f",
envvar="DBT_FULL_REFRESH",
help="If specified, dbt will drop incremental models and fully-recalculate the incremental table from the model definition.",
is_flag=True,
)
indirect_selection = click.option(
"--indirect-selection",
envvar="DBT_INDIRECT_SELECTION",
help="Select all tests that are adjacent to selected resources, even if they those resources have been explicitly selected.",
type=click.Choice(["eager", "cautious"], case_sensitive=False),
default="eager",
)
log_cache_events = click.option(
"--log-cache-events/--no-log-cache-events",
help="Enable verbose adapter cache logging.",
envvar="DBT_LOG_CACHE_EVENTS",
)
log_format = click.option(
"--log-format",
envvar="DBT_LOG_FORMAT",
help="Specify the log format, overriding the command's default.",
type=click.Choice(["text", "json", "default"], case_sensitive=False),
default="default",
)
log_path = click.option(
"--log-path",
envvar="DBT_LOG_PATH",
help="Configure the 'log-path'. Only applies this setting for the current run. Overrides the 'DBT_LOG_PATH' if it is set.",
type=click.Path(),
)
macro_debugging = click.option(
"--macro-debugging/--no-macro-debugging",
envvar="DBT_MACRO_DEBUGGING",
hidden=True,
)
models = click.option(
"-m",
"-s",
"models",
envvar=None,
help="Specify the nodes to include.",
multiple=True,
)
output = click.option(
"--output",
envvar=None,
help="TODO: No current help text",
type=click.Choice(["json", "name", "path", "selector"], case_sensitive=False),
default="name",
)
output_keys = click.option(
"--output-keys", envvar=None, help="TODO: No current help text", type=click.STRING
)
output_path = click.option(
"--output",
"-o",
envvar=None,
help="Specify the output path for the json report. By default, outputs to 'target/sources.json'",
type=click.Path(file_okay=True, dir_okay=False, writable=True),
default=PurePath.joinpath(Path.cwd(), "target/sources.json"),
)
parse_only = click.option(
"--parse-only",
envvar=None,
help="TODO: No help text currently available",
is_flag=True,
)
partial_parse = click.option(
"--partial-parse/--no-partial-parse",
envvar="DBT_PARTIAL_PARSE",
help="Allow for partial parsing by looking for and writing to a pickle file in the target directory. This overrides the user configuration file.",
default=True,
)
port = click.option(
"--port",
envvar=None,
help="Specify the port number for the docs server",
default=8080,
type=click.INT,
)
# TODO: The env var and name (reflected in flags) are corrections!
# The original name was `NO_PRINT` and used the env var `DBT_NO_PRINT`.
# Both of which break existing naming conventions.
# This will need to be fixed before use in the main codebase and communicated as a change to the community!
print = click.option(
"--print/--no-print",
envvar="DBT_PRINT",
help="Output all {{ print() }} macro calls.",
default=True,
)
printer_width = click.option(
"--printer-width",
envvar="DBT_PRINTER_WIDTH",
help="Sets the width of terminal output",
type=click.INT,
default=80,
)
profile = click.option(
"--profile",
envvar=None,
help="Which profile to load. Overrides setting in dbt_project.yml.",
)
profiles_dir = click.option(
"--profiles-dir",
envvar="DBT_PROFILES_DIR",
help="Which directory to look in for the profiles.yml file. If not set, dbt will look in the current working directory first, then HOME/.dbt/",
default=default_profiles_dir(),
type=click.Path(exists=True),
)
project_dir = click.option(
"--project-dir",
envvar=None,
help="Which directory to look in for the dbt_project.yml file. Default is the current working directory and its parents.",
default=default_project_dir(),
type=click.Path(exists=True),
)
quiet = click.option(
"--quiet/--no-quiet",
envvar="DBT_QUIET",
help="Suppress all non-error logging to stdout. Does not affect {{ print() }} macro calls.",
)
record_timing_info = click.option(
"--record-timing-info",
"-r",
envvar=None,
help="When this option is passed, dbt will output low-level timing stats to the specified file. Example: `--record-timing-info output.profile`",
type=click.Path(exists=False),
)
resource_type = click.option(
"--resource-type",
envvar=None,
help="TODO: No current help text",
type=click.Choice(
[
"metric",
"source",
"analysis",
"model",
"test",
"exposure",
"snapshot",
"seed",
"default",
"all",
],
case_sensitive=False,
),
default="default",
)
selector = click.option(
"--selector", envvar=None, help="The selector name to use, as defined in selectors.yml"
)
show = click.option(
"--show", envvar=None, help="Show a sample of the loaded data in the terminal", is_flag=True
)
skip_profile_setup = click.option(
"--skip-profile-setup", "-s", envvar=None, help="Skip interactive profile setup.", is_flag=True
)
# TODO: The env var and name (reflected in flags) are corrections!
# The original name was `ARTIFACT_STATE_PATH` and used the env var `DBT_ARTIFACT_STATE_PATH`.
# Both of which break existing naming conventions.
# This will need to be fixed before use in the main codebase and communicated as a change to the community!
state = click.option(
"--state",
envvar="DBT_STATE",
help="If set, use the given directory as the source for json files to compare with this project.",
type=click.Path(
dir_okay=True,
exists=True,
file_okay=False,
readable=True,
resolve_path=True,
),
)
static_parser = click.option(
"--static-parser/--no-static-parser",
envvar="DBT_STATIC_PARSER",
help="Use the static parser.",
default=True,
)
store_failures = click.option(
"--store-failures",
envvar="DBT_STORE_FAILURES",
help="Store test results (failing rows) in the database",
is_flag=True,
)
target = click.option(
"--target", "-t", envvar=None, help="Which target to load for the given profile"
)
target_path = click.option(
"--target-path",
envvar="DBT_TARGET_PATH",
help="Configure the 'target-path'. Only applies this setting for the current run. Overrides the 'DBT_TARGET_PATH' if it is set.",
type=click.Path(),
)
threads = click.option(
"--threads",
envvar=None,
help="Specify number of threads to use while executing models. Overrides settings in profiles.yml.",
default=1,
type=click.INT,
)
use_colors = click.option(
"--use-colors/--no-use-colors",
envvar="DBT_USE_COLORS",
help="Output is colorized by default and may also be set in a profile or at the command line.",
default=True,
)
use_experimental_parser = click.option(
"--use-experimental-parser/--no-use-experimental-parser",
envvar="DBT_USE_EXPERIMENTAL_PARSER",
help="Enable experimental parsing features.",
)
vars = click.option(
"--vars",
envvar=None,
help="Supply variables to the project. This argument overrides variables defined in your dbt_project.yml file. This argument should be a YAML string, eg. '{my_variable: my_value}'",
type=YAML(),
)
version = click.option(
"--version",
envvar=None,
help="Show version information",
is_flag=True,
)
version_check = click.option(
"--version-check/--no-version-check",
envvar="DBT_VERSION_CHECK",
help="Ensure dbt's version matches the one specified in the dbt_project.yml file ('require-dbt-version')",
default=True,
)
warn_error = click.option(
"--warn-error",
envvar="DBT_WARN_ERROR",
help="If dbt would normally warn, instead raise an exception. Examples include --select that selects nothing, deprecations, configurations with no associated models, invalid test configurations, and missing sources/refs in tests.",
default=None,
flag_value=True,
)
warn_error_options = click.option(
"--warn-error-options",
envvar="DBT_WARN_ERROR_OPTIONS",
default=None,
help="""If dbt would normally warn, instead raise an exception based on include/exclude configuration. Examples include --select that selects nothing, deprecations, configurations with no associated models, invalid test configurations,
and missing sources/refs in tests. This argument should be a YAML string, with keys 'include' or 'exclude'. eg. '{"include": "all", "exclude": ["NoNodesForSelectionCriteria"]}'""",
type=WarnErrorOptionsType(),
)
write_json = click.option(
"--write-json/--no-write-json",
envvar="DBT_WRITE_JSON",
help="Writing the manifest and run_results.json files to disk",
default=True,
)
write_manifest = click.option(
"--write-manifest/--no-write-manifest",
envvar=None,
help="TODO: No help text currently available",
default=True,
)

View File

@@ -1,11 +0,0 @@
from pathlib import Path
def default_project_dir():
paths = list(Path.cwd().parents)
paths.insert(0, Path.cwd())
return next((x for x in paths if (x / "dbt_project.yml").exists()), Path.cwd())
def default_profiles_dir():
return Path.cwd() if (Path.cwd() / "profiles.yml").exists() else Path.home() / ".dbt"

View File

@@ -1,19 +1 @@
# Clients README # Clients README
### Jinja
#### How are materializations defined
Model materializations are kept in `core/dbt/include/global_project/macros/materializations/models/`. Materializations are defined using syntax that isn't part of the Jinja standard library. These tags are referenced internally, and materializations can be overridden in user projects when users have specific needs.
```
-- Pseudocode for arguments
{% materialization <name>, <target name := one_of{default, adapter}> %}'
{% endmaterialization %}
```
These blocks are referred to Jinja extensions. Extensions are defined as part of the accepted Jinja code encapsulated within a dbt project. This includes system code used internally by dbt and user space (i.e. user-defined) macros. Extensions exist to help Jinja users create reusable code blocks or abstract objects--for us, materializations are a great use-case since we pass these around as arguments within dbt system code.
The code that defines this extension is a class `MaterializationExtension` and a `parse` routine. That code lives in [clients/jinja.py](https://github.com/dbt-labs/dbt-core/blob/main/core/dbt/clients/jinja.py). The routine
enables Jinja to parse (i.e. recognize) the unique comma separated arg structure our `materialization` tags exhibit (the `table, default` as seen above).

View File

@@ -1,15 +1,7 @@
import re import re
from collections import namedtuple from collections import namedtuple
from dbt.exceptions import ( import dbt.exceptions
BlockDefinitionNotAtTopError,
DbtInternalError,
MissingCloseTagError,
MissingControlFlowStartTagError,
NestedTagsError,
UnexpectedControlFlowEndTagError,
UnexpectedMacroEOFError,
)
def regex(pat): def regex(pat):
@@ -147,7 +139,10 @@ class TagIterator:
def _expect_match(self, expected_name, *patterns, **kwargs): def _expect_match(self, expected_name, *patterns, **kwargs):
match = self._first_match(*patterns, **kwargs) match = self._first_match(*patterns, **kwargs)
if match is None: if match is None:
raise UnexpectedMacroEOFError(expected_name, self.data[self.pos :]) msg = 'unexpected EOF, expected {}, got "{}"'.format(
expected_name, self.data[self.pos :]
)
dbt.exceptions.raise_compiler_error(msg)
return match return match
def handle_expr(self, match): def handle_expr(self, match):
@@ -261,7 +256,7 @@ class TagIterator:
elif block_type_name is not None: elif block_type_name is not None:
yield self.handle_tag(match) yield self.handle_tag(match)
else: else:
raise DbtInternalError( raise dbt.exceptions.InternalException(
"Invalid regex match in next_block, expected block start, " "Invalid regex match in next_block, expected block start, "
"expr start, or comment start" "expr start, or comment start"
) )
@@ -270,6 +265,13 @@ class TagIterator:
return self.find_tags() return self.find_tags()
duplicate_tags = (
"Got nested tags: {outer.block_type_name} (started at {outer.start}) did "
"not have a matching {{% end{outer.block_type_name} %}} before a "
"subsequent {inner.block_type_name} was found (started at {inner.start})"
)
_CONTROL_FLOW_TAGS = { _CONTROL_FLOW_TAGS = {
"if": "endif", "if": "endif",
"for": "endfor", "for": "endfor",
@@ -317,16 +319,33 @@ class BlockIterator:
found = self.stack.pop() found = self.stack.pop()
else: else:
expected = _CONTROL_FLOW_END_TAGS[tag.block_type_name] expected = _CONTROL_FLOW_END_TAGS[tag.block_type_name]
raise UnexpectedControlFlowEndTagError(tag, expected, self.tag_parser) dbt.exceptions.raise_compiler_error(
(
"Got an unexpected control flow end tag, got {} but "
"never saw a preceeding {} (@ {})"
).format(tag.block_type_name, expected, self.tag_parser.linepos(tag.start))
)
expected = _CONTROL_FLOW_TAGS[found] expected = _CONTROL_FLOW_TAGS[found]
if expected != tag.block_type_name: if expected != tag.block_type_name:
raise MissingControlFlowStartTagError(tag, expected, self.tag_parser) dbt.exceptions.raise_compiler_error(
(
"Got an unexpected control flow end tag, got {} but "
"expected {} next (@ {})"
).format(tag.block_type_name, expected, self.tag_parser.linepos(tag.start))
)
if tag.block_type_name in allowed_blocks: if tag.block_type_name in allowed_blocks:
if self.stack: if self.stack:
raise BlockDefinitionNotAtTopError(self.tag_parser, tag.start) dbt.exceptions.raise_compiler_error(
(
"Got a block definition inside control flow at {}. "
"All dbt block definitions must be at the top level"
).format(self.tag_parser.linepos(tag.start))
)
if self.current is not None: if self.current is not None:
raise NestedTagsError(outer=self.current, inner=tag) dbt.exceptions.raise_compiler_error(
duplicate_tags.format(outer=self.current, inner=tag)
)
if collect_raw_data: if collect_raw_data:
raw_data = self.data[self.last_position : tag.start] raw_data = self.data[self.last_position : tag.start]
self.last_position = tag.start self.last_position = tag.start
@@ -347,7 +366,11 @@ class BlockIterator:
if self.current: if self.current:
linecount = self.data[: self.current.end].count("\n") + 1 linecount = self.data[: self.current.end].count("\n") + 1
raise MissingCloseTagError(self.current.block_type_name, linecount) dbt.exceptions.raise_compiler_error(
(
"Reached EOF without finding a close tag for " "{} (searched from line {})"
).format(self.current.block_type_name, linecount)
)
if collect_raw_data: if collect_raw_data:
raw_data = self.data[self.last_position :] raw_data = self.data[self.last_position :]

View File

@@ -7,7 +7,7 @@ import json
import dbt.utils import dbt.utils
from typing import Iterable, List, Dict, Union, Optional, Any from typing import Iterable, List, Dict, Union, Optional, Any
from dbt.exceptions import DbtRuntimeError from dbt.exceptions import RuntimeException
BOM = BOM_UTF8.decode("utf-8") # '\ufeff' BOM = BOM_UTF8.decode("utf-8") # '\ufeff'
@@ -168,7 +168,7 @@ class ColumnTypeBuilder(Dict[str, NullableAgateType]):
return return
elif not isinstance(value, type(existing_type)): elif not isinstance(value, type(existing_type)):
# actual type mismatch! # actual type mismatch!
raise DbtRuntimeError( raise RuntimeException(
f"Tables contain columns with the same names ({key}), " f"Tables contain columns with the same names ({key}), "
f"but different types ({value} vs {existing_type})" f"but different types ({value} vs {existing_type})"
) )

View File

@@ -14,10 +14,10 @@ from dbt.events.types import (
) )
from dbt.exceptions import ( from dbt.exceptions import (
CommandResultError, CommandResultError,
GitCheckoutError, RuntimeException,
GitCloningError, bad_package_spec,
UnknownGitCloningProblemError, raise_git_cloning_error,
DbtRuntimeError, raise_git_cloning_problem,
) )
from packaging import version from packaging import version
@@ -27,6 +27,16 @@ def _is_commit(revision: str) -> bool:
return bool(re.match(r"\b[0-9a-f]{40}\b", revision)) return bool(re.match(r"\b[0-9a-f]{40}\b", revision))
def _raise_git_cloning_error(repo, revision, error):
stderr = error.stderr.strip()
if "usage: git" in stderr:
stderr = stderr.split("\nusage: git")[0]
if re.match("fatal: destination path '(.+)' already exists", stderr):
raise_git_cloning_error(error)
bad_package_spec(repo, revision, stderr)
def clone(repo, cwd, dirname=None, remove_git_dir=False, revision=None, subdirectory=None): def clone(repo, cwd, dirname=None, remove_git_dir=False, revision=None, subdirectory=None):
has_revision = revision is not None has_revision = revision is not None
is_commit = _is_commit(revision or "") is_commit = _is_commit(revision or "")
@@ -54,7 +64,7 @@ def clone(repo, cwd, dirname=None, remove_git_dir=False, revision=None, subdirec
try: try:
result = run_cmd(cwd, clone_cmd, env={"LC_ALL": "C"}) result = run_cmd(cwd, clone_cmd, env={"LC_ALL": "C"})
except CommandResultError as exc: except CommandResultError as exc:
raise GitCloningError(repo, revision, exc) _raise_git_cloning_error(repo, revision, exc)
if subdirectory: if subdirectory:
cwd_subdir = os.path.join(cwd, dirname or "") cwd_subdir = os.path.join(cwd, dirname or "")
@@ -62,7 +72,7 @@ def clone(repo, cwd, dirname=None, remove_git_dir=False, revision=None, subdirec
try: try:
run_cmd(cwd_subdir, clone_cmd_subdir) run_cmd(cwd_subdir, clone_cmd_subdir)
except CommandResultError as exc: except CommandResultError as exc:
raise GitCloningError(repo, revision, exc) _raise_git_cloning_error(repo, revision, exc)
if remove_git_dir: if remove_git_dir:
rmdir(os.path.join(dirname, ".git")) rmdir(os.path.join(dirname, ".git"))
@@ -105,7 +115,8 @@ def checkout(cwd, repo, revision=None):
try: try:
return _checkout(cwd, repo, revision) return _checkout(cwd, repo, revision)
except CommandResultError as exc: except CommandResultError as exc:
raise GitCheckoutError(repo=repo, revision=revision, error=exc) stderr = exc.stderr.strip()
bad_package_spec(repo, revision, stderr)
def get_current_sha(cwd): def get_current_sha(cwd):
@@ -134,7 +145,7 @@ def clone_and_checkout(
err = exc.stderr err = exc.stderr
exists = re.match("fatal: destination path '(.+)' already exists", err) exists = re.match("fatal: destination path '(.+)' already exists", err)
if not exists: if not exists:
raise UnknownGitCloningProblemError(repo) raise_git_cloning_problem(repo)
directory = None directory = None
start_sha = None start_sha = None
@@ -144,7 +155,7 @@ def clone_and_checkout(
else: else:
matches = re.match("Cloning into '(.+)'", err.decode("utf-8")) matches = re.match("Cloning into '(.+)'", err.decode("utf-8"))
if matches is None: if matches is None:
raise DbtRuntimeError(f'Error cloning {repo} - never saw "Cloning into ..." from git') raise RuntimeException(f'Error cloning {repo} - never saw "Cloning into ..." from git')
directory = matches.group(1) directory = matches.group(1)
fire_event(GitProgressPullingNewDependency(dir=directory)) fire_event(GitProgressPullingNewDependency(dir=directory))
full_path = os.path.join(cwd, directory) full_path = os.path.join(cwd, directory)

View File

@@ -25,26 +25,18 @@ from dbt.utils import (
) )
from dbt.clients._jinja_blocks import BlockIterator, BlockData, BlockTag from dbt.clients._jinja_blocks import BlockIterator, BlockData, BlockTag
from dbt.contracts.graph.nodes import GenericTestNode from dbt.contracts.graph.compiled import CompiledGenericTestNode
from dbt.contracts.graph.parsed import ParsedGenericTestNode
from dbt.exceptions import ( from dbt.exceptions import (
CaughtMacroError, InternalException,
CaughtMacroErrorWithNodeError, raise_compiler_error,
CompilationError, CompilationException,
DbtInternalError, invalid_materialization_argument,
MaterializationArgError,
JinjaRenderingError,
MacroReturn, MacroReturn,
MaterializtionMacroNotUsedError, JinjaRenderingException,
NoSupportedLanguagesFoundError, UndefinedMacroException,
UndefinedCompilationError,
UndefinedMacroError,
) )
from dbt import flags from dbt import flags
from dbt.node_types import ModelLanguage
SUPPORTED_LANG_ARG = jinja2.nodes.Name("supported_languages", "param")
def _linecache_inject(source, write): def _linecache_inject(source, write):
@@ -161,15 +153,15 @@ def quoted_native_concat(nodes):
except (ValueError, SyntaxError, MemoryError): except (ValueError, SyntaxError, MemoryError):
result = raw result = raw
if isinstance(raw, BoolMarker) and not isinstance(result, bool): if isinstance(raw, BoolMarker) and not isinstance(result, bool):
raise JinjaRenderingError(f"Could not convert value '{raw!s}' into type 'bool'") raise JinjaRenderingException(f"Could not convert value '{raw!s}' into type 'bool'")
if isinstance(raw, NumberMarker) and not _is_number(result): if isinstance(raw, NumberMarker) and not _is_number(result):
raise JinjaRenderingError(f"Could not convert value '{raw!s}' into type 'number'") raise JinjaRenderingException(f"Could not convert value '{raw!s}' into type 'number'")
return result return result
class NativeSandboxTemplate(jinja2.nativetypes.NativeTemplate): # mypy: ignore class NativeSandboxTemplate(jinja2.nativetypes.NativeTemplate): # mypy: ignore
environment_class = NativeSandboxEnvironment # type: ignore environment_class = NativeSandboxEnvironment
def render(self, *args, **kwargs): def render(self, *args, **kwargs):
"""Render the template to produce a native Python type. If the """Render the template to produce a native Python type. If the
@@ -241,12 +233,12 @@ class BaseMacroGenerator:
try: try:
yield yield
except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e: except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e:
raise CaughtMacroError(e) raise_compiler_error(str(e))
def call_macro(self, *args, **kwargs): def call_macro(self, *args, **kwargs):
# called from __call__ methods # called from __call__ methods
if self.context is None: if self.context is None:
raise DbtInternalError("Context is still None in call_macro!") raise InternalException("Context is still None in call_macro!")
assert self.context is not None assert self.context is not None
macro = self.get_macro() macro = self.get_macro()
@@ -273,7 +265,7 @@ class MacroStack(threading.local):
def pop(self, name): def pop(self, name):
got = self.call_stack.pop() got = self.call_stack.pop()
if got != name: if got != name:
raise DbtInternalError(f"popped {got}, expected {name}") raise InternalException(f"popped {got}, expected {name}")
class MacroGenerator(BaseMacroGenerator): class MacroGenerator(BaseMacroGenerator):
@@ -300,8 +292,8 @@ class MacroGenerator(BaseMacroGenerator):
try: try:
yield yield
except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e: except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e:
raise CaughtMacroErrorWithNodeError(exc=e, node=self.macro) raise_compiler_error(str(e), self.macro)
except CompilationError as e: except CompilationException as e:
e.stack.append(self.macro) e.stack.append(self.macro)
raise e raise e
@@ -309,13 +301,13 @@ class MacroGenerator(BaseMacroGenerator):
@contextmanager @contextmanager
def track_call(self): def track_call(self):
# This is only called from __call__ # This is only called from __call__
if self.stack is None: if self.stack is None or self.node is None:
yield yield
else: else:
unique_id = self.macro.unique_id unique_id = self.macro.unique_id
depth = self.stack.depth depth = self.stack.depth
# only mark depth=0 as a dependency, when creating this dependency we don't pass in stack # only mark depth=0 as a dependency
if depth == 0 and self.node: if depth == 0:
self.node.depends_on.add_macro(unique_id) self.node.depends_on.add_macro(unique_id)
self.stack.push(unique_id) self.stack.push(unique_id)
try: try:
@@ -372,19 +364,8 @@ class MaterializationExtension(jinja2.ext.Extension):
value = parser.parse_expression() value = parser.parse_expression()
adapter_name = value.value adapter_name = value.value
elif target.name == "supported_languages":
target.set_ctx("param")
node.args.append(target)
parser.stream.expect("assign")
languages = parser.parse_expression()
node.defaults.append(languages)
else: else:
raise MaterializationArgError(materialization_name, target.name) invalid_materialization_argument(materialization_name, target.name)
if SUPPORTED_LANG_ARG not in node.args:
node.args.append(SUPPORTED_LANG_ARG)
node.defaults.append(jinja2.nodes.List([jinja2.nodes.Const("sql")]))
node.name = get_materialization_macro_name(materialization_name, adapter_name) node.name = get_materialization_macro_name(materialization_name, adapter_name)
@@ -455,7 +436,7 @@ def create_undefined(node=None):
return self return self
def __reduce__(self): def __reduce__(self):
raise UndefinedCompilationError(name=self.name, node=node) raise_compiler_error(f"{self.name} is undefined", node=node)
return Undefined return Undefined
@@ -513,10 +494,10 @@ def catch_jinja(node=None) -> Iterator[None]:
yield yield
except jinja2.exceptions.TemplateSyntaxError as e: except jinja2.exceptions.TemplateSyntaxError as e:
e.translated = False e.translated = False
raise CompilationError(str(e), node) from e raise CompilationException(str(e), node) from e
except jinja2.exceptions.UndefinedError as e: except jinja2.exceptions.UndefinedError as e:
raise UndefinedMacroError(str(e), node) from e raise UndefinedMacroException(str(e), node) from e
except CompilationError as exc: except CompilationException as exc:
exc.add_node(node) exc.add_node(node)
raise raise
@@ -623,7 +604,7 @@ GENERIC_TEST_KWARGS_NAME = "_dbt_generic_test_kwargs"
def add_rendered_test_kwargs( def add_rendered_test_kwargs(
context: Dict[str, Any], context: Dict[str, Any],
node: GenericTestNode, node: Union[ParsedGenericTestNode, CompiledGenericTestNode],
capture_macros: bool = False, capture_macros: bool = False,
) -> None: ) -> None:
"""Render each of the test kwargs in the given context using the native """Render each of the test kwargs in the given context using the native
@@ -651,21 +632,3 @@ def add_rendered_test_kwargs(
# when the test node was created in _parse_generic_test. # when the test node was created in _parse_generic_test.
kwargs = deep_map_render(_convert_function, node.test_metadata.kwargs) kwargs = deep_map_render(_convert_function, node.test_metadata.kwargs)
context[GENERIC_TEST_KWARGS_NAME] = kwargs context[GENERIC_TEST_KWARGS_NAME] = kwargs
def get_supported_languages(node: jinja2.nodes.Macro) -> List[ModelLanguage]:
if "materialization" not in node.name:
raise MaterializtionMacroNotUsedError(node=node)
no_kwargs = not node.defaults
no_langs_found = SUPPORTED_LANG_ARG not in node.args
if no_kwargs or no_langs_found:
raise NoSupportedLanguagesFoundError(node=node)
lang_idx = node.args.index(SUPPORTED_LANG_ARG)
# indexing defaults from the end
# since supported_languages is a kwarg, and kwargs are at always after args
return [
ModelLanguage[item.value] for item in node.defaults[-(len(node.args) - lang_idx)].items
]

View File

@@ -1,6 +1,6 @@
import jinja2 import jinja2
from dbt.clients.jinja import get_environment from dbt.clients.jinja import get_environment
from dbt.exceptions import MacroNamespaceNotStringError, MacroNameNotStringError from dbt.exceptions import raise_compiler_error
def statically_extract_macro_calls(string, ctx, db_wrapper=None): def statically_extract_macro_calls(string, ctx, db_wrapper=None):
@@ -15,7 +15,7 @@ def statically_extract_macro_calls(string, ctx, db_wrapper=None):
if hasattr(func_call, "node") and hasattr(func_call.node, "name"): if hasattr(func_call, "node") and hasattr(func_call.node, "name"):
func_name = func_call.node.name func_name = func_call.node.name
else: else:
# func_call for dbt.current_timestamp macro # func_call for dbt_utils.current_timestamp macro
# Call( # Call(
# node=Getattr( # node=Getattr(
# node=Name( # node=Name(
@@ -117,14 +117,20 @@ def statically_parse_adapter_dispatch(func_call, ctx, db_wrapper):
func_name = kwarg.value.value func_name = kwarg.value.value
possible_macro_calls.append(func_name) possible_macro_calls.append(func_name)
else: else:
raise MacroNameNotStringError(kwarg_value=kwarg.value.value) raise_compiler_error(
f"The macro_name parameter ({kwarg.value.value}) "
"to adapter.dispatch was not a string"
)
elif kwarg.key == "macro_namespace": elif kwarg.key == "macro_namespace":
# This will remain to enable static resolution # This will remain to enable static resolution
kwarg_type = type(kwarg.value).__name__ kwarg_type = type(kwarg.value).__name__
if kwarg_type == "Const": if kwarg_type == "Const":
macro_namespace = kwarg.value.value macro_namespace = kwarg.value.value
else: else:
raise MacroNamespaceNotStringError(kwarg_type) raise_compiler_error(
"The macro_namespace parameter to adapter.dispatch "
f"is a {kwarg_type}, not a string"
)
# positional arguments # positional arguments
if packages_arg: if packages_arg:

View File

@@ -3,9 +3,9 @@ from typing import Any, Dict, List
import requests import requests
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import ( from dbt.events.types import (
RegistryProgressGETRequest, RegistryProgressMakingGETRequest,
RegistryProgressGETResponse, RegistryProgressGETResponse,
RegistryIndexProgressGETRequest, RegistryIndexProgressMakingGETRequest,
RegistryIndexProgressGETResponse, RegistryIndexProgressGETResponse,
RegistryResponseUnexpectedType, RegistryResponseUnexpectedType,
RegistryResponseMissingTopKeys, RegistryResponseMissingTopKeys,
@@ -14,7 +14,6 @@ from dbt.events.types import (
) )
from dbt.utils import memoized, _connection_exception_retry as connection_exception_retry from dbt.utils import memoized, _connection_exception_retry as connection_exception_retry
from dbt import deprecations from dbt import deprecations
from dbt import semver
import os import os
if os.getenv("DBT_PACKAGE_HUB_URL"): if os.getenv("DBT_PACKAGE_HUB_URL"):
@@ -38,7 +37,7 @@ def _get_with_retries(package_name, registry_base_url=None):
def _get(package_name, registry_base_url=None): def _get(package_name, registry_base_url=None):
url = _get_url(package_name, registry_base_url) url = _get_url(package_name, registry_base_url)
fire_event(RegistryProgressGETRequest(url=url)) fire_event(RegistryProgressMakingGETRequest(url=url))
# all exceptions from requests get caught in the retry logic so no need to wrap this here # all exceptions from requests get caught in the retry logic so no need to wrap this here
resp = requests.get(url, timeout=30) resp = requests.get(url, timeout=30)
fire_event(RegistryProgressGETResponse(url=url, resp_code=resp.status_code)) fire_event(RegistryProgressGETResponse(url=url, resp_code=resp.status_code))
@@ -90,15 +89,6 @@ def _get(package_name, registry_base_url=None):
fire_event(RegistryResponseExtraNestedKeys(response=response)) fire_event(RegistryResponseExtraNestedKeys(response=response))
raise requests.exceptions.ContentDecodingError(error_msg, response=resp) raise requests.exceptions.ContentDecodingError(error_msg, response=resp)
return response
_get_cached = memoized(_get_with_retries)
def package(package_name, registry_base_url=None) -> Dict[str, Any]:
# returns a dictionary of metadata for all versions of a package
response = _get_cached(package_name, registry_base_url)
# Either redirectnamespace or redirectname in the JSON response indicate a redirect # Either redirectnamespace or redirectname in the JSON response indicate a redirect
# redirectnamespace redirects based on package ownership # redirectnamespace redirects based on package ownership
# redirectname redirects based on package name # redirectname redirects based on package name
@@ -117,6 +107,16 @@ def package(package_name, registry_base_url=None) -> Dict[str, Any]:
new_nwo = use_namespace + "/" + use_name new_nwo = use_namespace + "/" + use_name
deprecations.warn("package-redirect", old_name=package_name, new_name=new_nwo) deprecations.warn("package-redirect", old_name=package_name, new_name=new_nwo)
return response
_get_cached = memoized(_get_with_retries)
def package(package_name, registry_base_url=None) -> Dict[str, Any]:
# returns a dictionary of metadata for all versions of a package
response = _get_cached(package_name, registry_base_url)
return response["versions"] return response["versions"]
@@ -126,43 +126,16 @@ def package_version(package_name, version, registry_base_url=None) -> Dict[str,
return response[version] return response[version]
def is_compatible_version(package_spec, dbt_version) -> bool: def get_available_versions(package_name) -> List["str"]:
require_dbt_version = package_spec.get("require_dbt_version")
if not require_dbt_version:
# if version requirements are missing or empty, assume any version is compatible
return True
else:
# determine whether dbt_version satisfies this package's require-dbt-version config
if not isinstance(require_dbt_version, list):
require_dbt_version = [require_dbt_version]
supported_versions = [
semver.VersionSpecifier.from_version_string(v) for v in require_dbt_version
]
return semver.versions_compatible(dbt_version, *supported_versions)
def get_compatible_versions(package_name, dbt_version, should_version_check) -> List["str"]:
# returns a list of all available versions of a package # returns a list of all available versions of a package
response = package(package_name) response = package(package_name)
# if the user doesn't care about installing compatible versions, just return them all
if not should_version_check:
return list(response) return list(response)
# otherwise, only return versions that are compatible with the installed version of dbt-core
else:
compatible_versions = [
pkg_version
for pkg_version, info in response.items()
if is_compatible_version(info, dbt_version)
]
return compatible_versions
def _get_index(registry_base_url=None): def _get_index(registry_base_url=None):
url = _get_url("index", registry_base_url) url = _get_url("index", registry_base_url)
fire_event(RegistryIndexProgressGETRequest(url=url)) fire_event(RegistryIndexProgressMakingGETRequest(url=url))
# all exceptions from requests get caught in the retry logic so no need to wrap this here # all exceptions from requests get caught in the retry logic so no need to wrap this here
resp = requests.get(url, timeout=30) resp = requests.get(url, timeout=30)
fire_event(RegistryIndexProgressGETResponse(url=url, resp_code=resp.status_code)) fire_event(RegistryIndexProgressGETResponse(url=url, resp_code=resp.status_code))

View File

@@ -12,15 +12,14 @@ import tarfile
import requests import requests
import stat import stat
from typing import Type, NoReturn, List, Optional, Dict, Any, Tuple, Callable, Union from typing import Type, NoReturn, List, Optional, Dict, Any, Tuple, Callable, Union
from pathspec import PathSpec # type: ignore
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import ( from dbt.events.types import (
SystemErrorRetrievingModTime, SystemErrorRetrievingModTime,
SystemCouldNotWrite, SystemCouldNotWrite,
SystemExecutingCmd, SystemExecutingCmd,
SystemStdOut, SystemStdOutMsg,
SystemStdErr, SystemStdErrMsg,
SystemReportReturnCode, SystemReportReturnCode,
) )
import dbt.exceptions import dbt.exceptions
@@ -37,7 +36,6 @@ def find_matching(
root_path: str, root_path: str,
relative_paths_to_search: List[str], relative_paths_to_search: List[str],
file_pattern: str, file_pattern: str,
ignore_spec: Optional[PathSpec] = None,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
""" """
Given an absolute `root_path`, a list of relative paths to that Given an absolute `root_path`, a list of relative paths to that
@@ -59,30 +57,19 @@ def find_matching(
reobj = re.compile(regex, re.IGNORECASE) reobj = re.compile(regex, re.IGNORECASE)
for relative_path_to_search in relative_paths_to_search: for relative_path_to_search in relative_paths_to_search:
# potential speedup for ignore_spec
# if ignore_spec.matches(relative_path_to_search):
# continue
absolute_path_to_search = os.path.join(root_path, relative_path_to_search) absolute_path_to_search = os.path.join(root_path, relative_path_to_search)
walk_results = os.walk(absolute_path_to_search) walk_results = os.walk(absolute_path_to_search)
for current_path, subdirectories, local_files in walk_results: for current_path, subdirectories, local_files in walk_results:
# potential speedup for ignore_spec
# relative_dir = os.path.relpath(current_path, root_path) + os.sep
# if ignore_spec.match(relative_dir):
# continue
for local_file in local_files: for local_file in local_files:
absolute_path = os.path.join(current_path, local_file) absolute_path = os.path.join(current_path, local_file)
relative_path = os.path.relpath(absolute_path, absolute_path_to_search) relative_path = os.path.relpath(absolute_path, absolute_path_to_search)
relative_path_to_root = os.path.join(relative_path_to_search, relative_path)
modification_time = 0.0 modification_time = 0.0
try: try:
modification_time = os.path.getmtime(absolute_path) modification_time = os.path.getmtime(absolute_path)
except OSError: except OSError:
fire_event(SystemErrorRetrievingModTime(path=absolute_path)) fire_event(SystemErrorRetrievingModTime(path=absolute_path))
if reobj.match(local_file) and ( if reobj.match(local_file):
not ignore_spec or not ignore_spec.match_file(relative_path_to_root)
):
matching.append( matching.append(
{ {
"searched_path": relative_path_to_search, "searched_path": relative_path_to_search,
@@ -144,8 +131,7 @@ def make_symlink(source: str, link_path: str) -> None:
Create a symlink at `link_path` referring to `source`. Create a symlink at `link_path` referring to `source`.
""" """
if not supports_symlinks(): if not supports_symlinks():
# TODO: why not import these at top? dbt.exceptions.system_error("create a symbolic link")
raise dbt.exceptions.SymbolicLinkError()
os.symlink(source, link_path) os.symlink(source, link_path)
@@ -178,7 +164,7 @@ def write_file(path: str, contents: str = "") -> bool:
reason = "Path was possibly too long" reason = "Path was possibly too long"
# all our hard work and the path was still too long. Log and # all our hard work and the path was still too long. Log and
# continue. # continue.
fire_event(SystemCouldNotWrite(path=path, reason=reason, exc=str(exc))) fire_event(SystemCouldNotWrite(path=path, reason=reason, exc=exc))
else: else:
raise raise
return True return True
@@ -260,7 +246,6 @@ def _supports_long_paths() -> bool:
# https://stackoverflow.com/a/35097999/11262881 # https://stackoverflow.com/a/35097999/11262881
# I don't know exaclty what he means, but I am inclined to believe him as # I don't know exaclty what he means, but I am inclined to believe him as
# he's pretty active on Python windows bugs! # he's pretty active on Python windows bugs!
else:
try: try:
dll = WinDLL("ntdll") dll = WinDLL("ntdll")
except OSError: # I don't think this happens? you need ntdll to run python except OSError: # I don't think this happens? you need ntdll to run python
@@ -412,7 +397,7 @@ def _interpret_oserror(exc: OSError, cwd: str, cmd: List[str]) -> NoReturn:
_handle_posix_error(exc, cwd, cmd) _handle_posix_error(exc, cwd, cmd)
# this should not be reachable, raise _something_ at least! # this should not be reachable, raise _something_ at least!
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
"Unhandled exception in _interpret_oserror: {}".format(exc) "Unhandled exception in _interpret_oserror: {}".format(exc)
) )
@@ -441,8 +426,8 @@ def run_cmd(cwd: str, cmd: List[str], env: Optional[Dict[str, Any]] = None) -> T
except OSError as exc: except OSError as exc:
_interpret_oserror(exc, cwd, cmd) _interpret_oserror(exc, cwd, cmd)
fire_event(SystemStdOut(bmsg=out)) fire_event(SystemStdOutMsg(bmsg=out))
fire_event(SystemStdErr(bmsg=err)) fire_event(SystemStdErrMsg(bmsg=err))
if proc.returncode != 0: if proc.returncode != 0:
fire_event(SystemReportReturnCode(returncode=proc.returncode)) fire_event(SystemReportReturnCode(returncode=proc.returncode))
@@ -458,11 +443,7 @@ def download_with_retries(
connection_exception_retry(download_fn, 5) connection_exception_retry(download_fn, 5)
def download( def download(url: str, path: str, timeout: Optional[Union[float, tuple]] = None) -> None:
url: str,
path: str,
timeout: Optional[Union[float, Tuple[float, float], Tuple[float, None]]] = None,
) -> None:
path = convert_path(path) path = convert_path(path)
connection_timeout = timeout or float(os.getenv("DBT_HTTP_TIMEOUT", 10)) connection_timeout = timeout or float(os.getenv("DBT_HTTP_TIMEOUT", 10))
response = requests.get(url, timeout=connection_timeout) response = requests.get(url, timeout=connection_timeout)

View File

@@ -51,7 +51,7 @@ def safe_load(contents) -> Optional[Dict[str, Any]]:
return yaml.load(contents, Loader=SafeLoader) return yaml.load(contents, Loader=SafeLoader)
def load_yaml_text(contents, path=None): def load_yaml_text(contents):
try: try:
return safe_load(contents) return safe_load(contents)
except (yaml.scanner.ScannerError, yaml.YAMLError) as e: except (yaml.scanner.ScannerError, yaml.YAMLError) as e:
@@ -60,4 +60,4 @@ def load_yaml_text(contents, path=None):
else: else:
error = str(e) error = str(e)
raise dbt.exceptions.DbtValidationError(error) raise dbt.exceptions.ValidationException(error)

View File

@@ -1,6 +1,6 @@
import os import os
from collections import defaultdict from collections import defaultdict
from typing import List, Dict, Any, Tuple, Optional from typing import List, Dict, Any, Tuple, cast, Optional
import networkx as nx # type: ignore import networkx as nx # type: ignore
import pickle import pickle
@@ -12,30 +12,38 @@ from dbt.clients import jinja
from dbt.clients.system import make_directory from dbt.clients.system import make_directory
from dbt.context.providers import generate_runtime_model_context from dbt.context.providers import generate_runtime_model_context
from dbt.contracts.graph.manifest import Manifest, UniqueID from dbt.contracts.graph.manifest import Manifest, UniqueID
from dbt.contracts.graph.nodes import ( from dbt.contracts.graph.compiled import (
ManifestNode, COMPILED_TYPES,
ManifestSQLNode, CompiledGenericTestNode,
GenericTestNode,
GraphMemberNode, GraphMemberNode,
InjectedCTE, InjectedCTE,
SeedNode, ManifestNode,
NonSourceCompiledNode,
) )
from dbt.contracts.graph.parsed import ParsedNode
from dbt.exceptions import ( from dbt.exceptions import (
GraphDependencyNotFoundError, dependency_not_found,
DbtInternalError, InternalException,
DbtRuntimeError, RuntimeException,
) )
from dbt.graph import Graph from dbt.graph import Graph
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import FoundStats, WritingInjectedSQLForNode from dbt.events.types import FoundStats, CompilingNode, WritingInjectedSQLForNode
from dbt.events.contextvars import get_node_info from dbt.node_types import NodeType
from dbt.node_types import NodeType, ModelLanguage
from dbt.events.format import pluralize from dbt.events.format import pluralize
import dbt.tracking import dbt.tracking
graph_file_name = "graph.gpickle" graph_file_name = "graph.gpickle"
def _compiled_type_for(model: ParsedNode):
if type(model) not in COMPILED_TYPES:
raise InternalException(
f"Asked to compile {type(model)} node, but it has no compiled form"
)
return COMPILED_TYPES[type(model)]
def print_compile_stats(stats): def print_compile_stats(stats):
names = { names = {
NodeType.Model: "model", NodeType.Model: "model",
@@ -48,7 +56,6 @@ def print_compile_stats(stats):
NodeType.Source: "source", NodeType.Source: "source",
NodeType.Exposure: "exposure", NodeType.Exposure: "exposure",
NodeType.Metric: "metric", NodeType.Metric: "metric",
NodeType.Entity: "entity",
} }
results = {k: 0 for k in names.keys()} results = {k: 0 for k in names.keys()}
@@ -84,8 +91,6 @@ def _generate_stats(manifest: Manifest):
stats[exposure.resource_type] += 1 stats[exposure.resource_type] += 1
for metric in manifest.metrics.values(): for metric in manifest.metrics.values():
stats[metric.resource_type] += 1 stats[metric.resource_type] += 1
for entity in manifest.entities.values():
stats[entity.resource_type] += 1
for macro in manifest.macros.values(): for macro in manifest.macros.values():
stats[macro.resource_type] += 1 stats[macro.resource_type] += 1
return stats return stats
@@ -171,15 +176,14 @@ class Compiler:
# a dict for jinja rendering of SQL # a dict for jinja rendering of SQL
def _create_node_context( def _create_node_context(
self, self,
node: ManifestSQLNode, node: NonSourceCompiledNode,
manifest: Manifest, manifest: Manifest,
extra_context: Dict[str, Any], extra_context: Dict[str, Any],
) -> Dict[str, Any]: ) -> Dict[str, Any]:
context = generate_runtime_model_context(node, self.config, manifest) context = generate_runtime_model_context(node, self.config, manifest)
context.update(extra_context) context.update(extra_context)
if isinstance(node, CompiledGenericTestNode):
if isinstance(node, GenericTestNode):
# for test nodes, add a special keyword args value to the context # for test nodes, add a special keyword args value to the context
jinja.add_rendered_test_kwargs(context, node) jinja.add_rendered_test_kwargs(context, node)
@@ -190,6 +194,14 @@ class Compiler:
relation_cls = adapter.Relation relation_cls = adapter.Relation
return relation_cls.add_ephemeral_prefix(name) return relation_cls.add_ephemeral_prefix(name)
def _get_relation_name(self, node: ParsedNode):
relation_name = None
if node.is_relational and not node.is_ephemeral_model:
adapter = get_adapter(self.config)
relation_cls = adapter.Relation
relation_name = str(relation_cls.create_from(self.config, node))
return relation_name
def _inject_ctes_into_sql(self, sql: str, ctes: List[InjectedCTE]) -> str: def _inject_ctes_into_sql(self, sql: str, ctes: List[InjectedCTE]) -> str:
""" """
`ctes` is a list of InjectedCTEs like: `ctes` is a list of InjectedCTEs like:
@@ -248,10 +260,10 @@ class Compiler:
def _recursively_prepend_ctes( def _recursively_prepend_ctes(
self, self,
model: ManifestSQLNode, model: NonSourceCompiledNode,
manifest: Manifest, manifest: Manifest,
extra_context: Optional[Dict[str, Any]], extra_context: Optional[Dict[str, Any]],
) -> Tuple[ManifestSQLNode, List[InjectedCTE]]: ) -> Tuple[NonSourceCompiledNode, List[InjectedCTE]]:
"""This method is called by the 'compile_node' method. Starting """This method is called by the 'compile_node' method. Starting
from the node that it is passed in, it will recursively call from the node that it is passed in, it will recursively call
itself using the 'extra_ctes'. The 'ephemeral' models do itself using the 'extra_ctes'. The 'ephemeral' models do
@@ -259,14 +271,13 @@ class Compiler:
are rolled up into the models that refer to them by are rolled up into the models that refer to them by
inserting CTEs into the SQL. inserting CTEs into the SQL.
""" """
if model.compiled_code is None: if model.compiled_sql is None:
raise DbtRuntimeError("Cannot inject ctes into an unparsed node", model) raise RuntimeException("Cannot inject ctes into an unparsed node", model)
if model.extra_ctes_injected: if model.extra_ctes_injected:
return (model, model.extra_ctes) return (model, model.extra_ctes)
# Just to make it plain that nothing is actually injected for this case # Just to make it plain that nothing is actually injected for this case
if not model.extra_ctes: if not model.extra_ctes:
if not isinstance(model, SeedNode):
model.extra_ctes_injected = True model.extra_ctes_injected = True
manifest.update_node(model) manifest.update_node(model)
return (model, model.extra_ctes) return (model, model.extra_ctes)
@@ -281,19 +292,20 @@ class Compiler:
# ephemeral model. # ephemeral model.
for cte in model.extra_ctes: for cte in model.extra_ctes:
if cte.id not in manifest.nodes: if cte.id not in manifest.nodes:
raise DbtInternalError( raise InternalException(
f"During compilation, found a cte reference that " f"During compilation, found a cte reference that "
f"could not be resolved: {cte.id}" f"could not be resolved: {cte.id}"
) )
cte_model = manifest.nodes[cte.id] cte_model = manifest.nodes[cte.id]
assert not isinstance(cte_model, SeedNode)
if not cte_model.is_ephemeral_model: if not cte_model.is_ephemeral_model:
raise DbtInternalError(f"{cte.id} is not ephemeral") raise InternalException(f"{cte.id} is not ephemeral")
# This model has already been compiled, so it's been # This model has already been compiled, so it's been
# through here before # through here before
if getattr(cte_model, "compiled", False): if getattr(cte_model, "compiled", False):
assert isinstance(cte_model, tuple(COMPILED_TYPES.values()))
cte_model = cast(NonSourceCompiledNode, cte_model)
new_prepended_ctes = cte_model.extra_ctes new_prepended_ctes = cte_model.extra_ctes
# if the cte_model isn't compiled, i.e. first time here # if the cte_model isn't compiled, i.e. first time here
@@ -312,78 +324,64 @@ class Compiler:
_extend_prepended_ctes(prepended_ctes, new_prepended_ctes) _extend_prepended_ctes(prepended_ctes, new_prepended_ctes)
new_cte_name = self.add_ephemeral_prefix(cte_model.name) new_cte_name = self.add_ephemeral_prefix(cte_model.name)
rendered_sql = cte_model._pre_injected_sql or cte_model.compiled_code rendered_sql = cte_model._pre_injected_sql or cte_model.compiled_sql
sql = f" {new_cte_name} as (\n{rendered_sql}\n)" sql = f" {new_cte_name} as (\n{rendered_sql}\n)"
_add_prepended_cte(prepended_ctes, InjectedCTE(id=cte.id, sql=sql)) _add_prepended_cte(prepended_ctes, InjectedCTE(id=cte.id, sql=sql))
injected_sql = self._inject_ctes_into_sql( injected_sql = self._inject_ctes_into_sql(
model.compiled_code, model.compiled_sql,
prepended_ctes, prepended_ctes,
) )
model._pre_injected_sql = model.compiled_code model._pre_injected_sql = model.compiled_sql
model.compiled_code = injected_sql model.compiled_sql = injected_sql
model.extra_ctes_injected = True model.extra_ctes_injected = True
model.extra_ctes = prepended_ctes model.extra_ctes = prepended_ctes
model.validate(model.to_dict(omit_none=True)) model.validate(model.to_dict(omit_none=True))
manifest.update_node(model) manifest.update_node(model)
return model, prepended_ctes return model, prepended_ctes
# Sets compiled fields in the ManifestSQLNode passed in, # creates a compiled_node from the ManifestNode passed in,
# creates a "context" dictionary for jinja rendering, # creates a "context" dictionary for jinja rendering,
# and then renders the "compiled_code" using the node, the # and then renders the "compiled_sql" using the node, the
# raw_code and the context. # raw_sql and the context.
def _compile_node( def _compile_node(
self, self,
node: ManifestSQLNode, node: ManifestNode,
manifest: Manifest, manifest: Manifest,
extra_context: Optional[Dict[str, Any]] = None, extra_context: Optional[Dict[str, Any]] = None,
) -> ManifestSQLNode: ) -> NonSourceCompiledNode:
if extra_context is None: if extra_context is None:
extra_context = {} extra_context = {}
fire_event(CompilingNode(unique_id=node.unique_id))
data = node.to_dict(omit_none=True) data = node.to_dict(omit_none=True)
data.update( data.update(
{ {
"compiled": False, "compiled": False,
"compiled_code": None, "compiled_sql": None,
"extra_ctes_injected": False, "extra_ctes_injected": False,
"extra_ctes": [], "extra_ctes": [],
} }
) )
compiled_node = _compiled_type_for(node).from_dict(data)
if node.language == ModelLanguage.python: context = self._create_node_context(compiled_node, manifest, extra_context)
# TODO could we also 'minify' this code at all? just aesthetic, not functional
# quoating seems like something very specific to sql so far compiled_node.compiled_sql = jinja.get_rendered(
# for all python implementations we are seeing there's no quating. node.raw_sql,
# TODO try to find better way to do this, given that
original_quoting = self.config.quoting
self.config.quoting = {key: False for key in original_quoting.keys()}
context = self._create_node_context(node, manifest, extra_context)
postfix = jinja.get_rendered(
"{{ py_script_postfix(model) }}",
context,
node,
)
# we should NOT jinja render the python model's 'raw code'
node.compiled_code = f"{node.raw_code}\n\n{postfix}"
# restore quoting settings in the end since context is lazy evaluated
self.config.quoting = original_quoting
else:
context = self._create_node_context(node, manifest, extra_context)
node.compiled_code = jinja.get_rendered(
node.raw_code,
context, context,
node, node,
) )
node.compiled = True compiled_node.relation_name = self._get_relation_name(node)
return node compiled_node.compiled = True
return compiled_node
def write_graph_file(self, linker: Linker, manifest: Manifest): def write_graph_file(self, linker: Linker, manifest: Manifest):
filename = graph_file_name filename = graph_file_name
@@ -399,12 +397,8 @@ class Compiler:
linker.dependency(node.unique_id, (manifest.nodes[dependency].unique_id)) linker.dependency(node.unique_id, (manifest.nodes[dependency].unique_id))
elif dependency in manifest.sources: elif dependency in manifest.sources:
linker.dependency(node.unique_id, (manifest.sources[dependency].unique_id)) linker.dependency(node.unique_id, (manifest.sources[dependency].unique_id))
elif dependency in manifest.metrics:
linker.dependency(node.unique_id, (manifest.metrics[dependency].unique_id))
elif dependency in manifest.entities:
linker.dependency(node.unique_id, (manifest.entities[dependency].unique_id))
else: else:
raise GraphDependencyNotFoundError(node, dependency) dependency_not_found(node, dependency)
def link_graph(self, linker: Linker, manifest: Manifest, add_test_edges: bool = False): def link_graph(self, linker: Linker, manifest: Manifest, add_test_edges: bool = False):
for source in manifest.sources.values(): for source in manifest.sources.values():
@@ -415,8 +409,6 @@ class Compiler:
self.link_node(linker, exposure, manifest) self.link_node(linker, exposure, manifest)
for metric in manifest.metrics.values(): for metric in manifest.metrics.values():
self.link_node(linker, metric, manifest) self.link_node(linker, metric, manifest)
for entity in manifest.entities.values():
self.link_node(linker, entity, manifest)
cycle = linker.find_cycles() cycle = linker.find_cycles()
@@ -493,28 +485,25 @@ class Compiler:
return Graph(linker.graph) return Graph(linker.graph)
# writes the "compiled_code" into the target/compiled directory # writes the "compiled_sql" into the target/compiled directory
def _write_node(self, node: ManifestSQLNode) -> ManifestSQLNode: def _write_node(self, node: NonSourceCompiledNode) -> ManifestNode:
if not node.extra_ctes_injected or node.resource_type in ( if not node.extra_ctes_injected or node.resource_type == NodeType.Snapshot:
NodeType.Snapshot,
NodeType.Seed,
):
return node return node
fire_event(WritingInjectedSQLForNode(node_info=get_node_info())) fire_event(WritingInjectedSQLForNode(unique_id=node.unique_id))
if node.compiled_code: if node.compiled_sql:
node.compiled_path = node.write_node( node.compiled_path = node.write_node(
self.config.target_path, "compiled", node.compiled_code self.config.target_path, "compiled", node.compiled_sql
) )
return node return node
def compile_node( def compile_node(
self, self,
node: ManifestSQLNode, node: ManifestNode,
manifest: Manifest, manifest: Manifest,
extra_context: Optional[Dict[str, Any]] = None, extra_context: Optional[Dict[str, Any]] = None,
write: bool = True, write: bool = True,
) -> ManifestSQLNode: ) -> NonSourceCompiledNode:
"""This is the main entry point into this code. It's called by """This is the main entry point into this code. It's called by
CompileRunner.compile, GenericRPCRunner.compile, and CompileRunner.compile, GenericRPCRunner.compile, and
RunTask.get_hook_sql. It calls '_compile_node' to convert RunTask.get_hook_sql. It calls '_compile_node' to convert

View File

@@ -9,14 +9,12 @@ from dbt.clients.system import load_file_contents
from dbt.clients.yaml_helper import load_yaml_text from dbt.clients.yaml_helper import load_yaml_text
from dbt.contracts.connection import Credentials, HasCredentials from dbt.contracts.connection import Credentials, HasCredentials
from dbt.contracts.project import ProfileConfig, UserConfig from dbt.contracts.project import ProfileConfig, UserConfig
from dbt.exceptions import ( from dbt.exceptions import CompilationException
CompilationError, from dbt.exceptions import DbtProfileError
DbtProfileError, from dbt.exceptions import DbtProjectError
DbtProjectError, from dbt.exceptions import ValidationException
DbtValidationError, from dbt.exceptions import RuntimeException
DbtRuntimeError, from dbt.exceptions import validator_error_message
ProfileConfigError,
)
from dbt.events.types import MissingProfileTarget from dbt.events.types import MissingProfileTarget
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.utils import coerce_dict_str from dbt.utils import coerce_dict_str
@@ -25,6 +23,8 @@ from .renderer import ProfileRenderer
DEFAULT_THREADS = 1 DEFAULT_THREADS = 1
DEFAULT_PROFILES_DIR = os.path.join(os.path.expanduser("~"), ".dbt")
INVALID_PROFILE_MESSAGE = """ INVALID_PROFILE_MESSAGE = """
dbt encountered an error while trying to read your profiles.yml file. dbt encountered an error while trying to read your profiles.yml file.
@@ -44,7 +44,7 @@ defined in your profiles.yml file. You can find profiles.yml here:
{profiles_file}/profiles.yml {profiles_file}/profiles.yml
""".format( """.format(
profiles_file=flags.DEFAULT_PROFILES_DIR profiles_file=DEFAULT_PROFILES_DIR
) )
@@ -60,9 +60,9 @@ def read_profile(profiles_dir: str) -> Dict[str, Any]:
msg = f"The profiles.yml file at {path} is empty" msg = f"The profiles.yml file at {path} is empty"
raise DbtProfileError(INVALID_PROFILE_MESSAGE.format(error_string=msg)) raise DbtProfileError(INVALID_PROFILE_MESSAGE.format(error_string=msg))
return yaml_content return yaml_content
except DbtValidationError as e: except ValidationException as e:
msg = INVALID_PROFILE_MESSAGE.format(error_string=e) msg = INVALID_PROFILE_MESSAGE.format(error_string=e)
raise DbtValidationError(msg) from e raise ValidationException(msg) from e
return {} return {}
@@ -75,7 +75,7 @@ def read_user_config(directory: str) -> UserConfig:
if user_config is not None: if user_config is not None:
UserConfig.validate(user_config) UserConfig.validate(user_config)
return UserConfig.from_dict(user_config) return UserConfig.from_dict(user_config)
except (DbtRuntimeError, ValidationError): except (RuntimeException, ValidationError):
pass pass
return UserConfig() return UserConfig()
@@ -158,7 +158,7 @@ class Profile(HasCredentials):
dct = self.to_profile_info(serialize_credentials=True) dct = self.to_profile_info(serialize_credentials=True)
ProfileConfig.validate(dct) ProfileConfig.validate(dct)
except ValidationError as exc: except ValidationError as exc:
raise ProfileConfigError(exc) from exc raise DbtProfileError(validator_error_message(exc)) from exc
@staticmethod @staticmethod
def _credentials_from_profile( def _credentials_from_profile(
@@ -182,8 +182,8 @@ class Profile(HasCredentials):
data = cls.translate_aliases(profile) data = cls.translate_aliases(profile)
cls.validate(data) cls.validate(data)
credentials = cls.from_dict(data) credentials = cls.from_dict(data)
except (DbtRuntimeError, ValidationError) as e: except (RuntimeException, ValidationError) as e:
msg = str(e) if isinstance(e, DbtRuntimeError) else e.message msg = str(e) if isinstance(e, RuntimeException) else e.message
raise DbtProfileError( raise DbtProfileError(
'Credentials in profile "{}", target "{}" invalid: {}'.format( 'Credentials in profile "{}", target "{}" invalid: {}'.format(
profile_name, target_name, msg profile_name, target_name, msg
@@ -299,7 +299,7 @@ class Profile(HasCredentials):
try: try:
profile_data = renderer.render_data(raw_profile_data) profile_data = renderer.render_data(raw_profile_data)
except CompilationError as exc: except CompilationException as exc:
raise DbtProfileError(str(exc)) from exc raise DbtProfileError(str(exc)) from exc
return target_name, profile_data return target_name, profile_data

View File

@@ -15,20 +15,20 @@ from typing_extensions import Protocol, runtime_checkable
import hashlib import hashlib
import os import os
from dbt import flags, deprecations from dbt import deprecations
from dbt.clients.system import path_exists, resolve_path_from_base, load_file_contents from dbt.clients.system import resolve_path_from_base
from dbt.clients.system import path_exists
from dbt.clients.system import load_file_contents
from dbt.clients.yaml_helper import load_yaml_text from dbt.clients.yaml_helper import load_yaml_text
from dbt.contracts.connection import QueryComment from dbt.contracts.connection import QueryComment
from dbt.exceptions import ( from dbt.exceptions import DbtProjectError
DbtProjectError, from dbt.exceptions import SemverException
SemverError, from dbt.exceptions import validator_error_message
ProjectContractBrokenError, from dbt.exceptions import RuntimeException
ProjectContractError,
DbtRuntimeError,
)
from dbt.graph import SelectionSpec from dbt.graph import SelectionSpec
from dbt.helper_types import NoValue from dbt.helper_types import NoValue
from dbt.semver import VersionSpecifier, versions_compatible from dbt.semver import VersionSpecifier
from dbt.semver import versions_compatible
from dbt.version import get_installed_version from dbt.version import get_installed_version
from dbt.utils import MultiDict from dbt.utils import MultiDict
from dbt.node_types import NodeType from dbt.node_types import NodeType
@@ -132,23 +132,12 @@ def _all_source_paths(
analysis_paths: List[str], analysis_paths: List[str],
macro_paths: List[str], macro_paths: List[str],
) -> List[str]: ) -> List[str]:
# We need to turn a list of lists into just a list, then convert to a set to return list(chain(model_paths, seed_paths, snapshot_paths, analysis_paths, macro_paths))
# get only unique elements, then back to a list
return list(
set(list(chain(model_paths, seed_paths, snapshot_paths, analysis_paths, macro_paths)))
)
T = TypeVar("T") T = TypeVar("T")
def flag_or(flag: Optional[T], value: Optional[T], default: T) -> T:
if flag is None:
return value_or(value, default)
else:
return flag
def value_or(value: Optional[T], default: T) -> T: def value_or(value: Optional[T], default: T) -> T:
if value is None: if value is None:
return default return default
@@ -219,7 +208,7 @@ def _get_required_version(
try: try:
dbt_version = _parse_versions(dbt_raw_version) dbt_version = _parse_versions(dbt_raw_version)
except SemverError as e: except SemverException as e:
raise DbtProjectError(str(e)) from e raise DbtProjectError(str(e)) from e
if verify_version: if verify_version:
@@ -248,7 +237,7 @@ class PartialProject(RenderComponents):
project_name: Optional[str] = field( project_name: Optional[str] = field(
metadata=dict( metadata=dict(
description=( description=(
"The name of the project. This should always be set and will not be rendered" "The name of the project. This should always be set and will not " "be rendered"
) )
) )
) )
@@ -325,7 +314,7 @@ class PartialProject(RenderComponents):
ProjectContract.validate(rendered.project_dict) ProjectContract.validate(rendered.project_dict)
cfg = ProjectContract.from_dict(rendered.project_dict) cfg = ProjectContract.from_dict(rendered.project_dict)
except ValidationError as e: except ValidationError as e:
raise ProjectContractError(e) from e raise DbtProjectError(validator_error_message(e)) from e
# name/version are required in the Project definition, so we can assume # name/version are required in the Project definition, so we can assume
# they are present # they are present
name = cfg.name name = cfg.name
@@ -363,9 +352,9 @@ class PartialProject(RenderComponents):
docs_paths: List[str] = value_or(cfg.docs_paths, all_source_paths) docs_paths: List[str] = value_or(cfg.docs_paths, all_source_paths)
asset_paths: List[str] = value_or(cfg.asset_paths, []) asset_paths: List[str] = value_or(cfg.asset_paths, [])
target_path: str = flag_or(flags.TARGET_PATH, cfg.target_path, "target") target_path: str = value_or(cfg.target_path, "target")
clean_targets: List[str] = value_or(cfg.clean_targets, [target_path]) clean_targets: List[str] = value_or(cfg.clean_targets, [target_path])
log_path: str = flag_or(flags.LOG_PATH, cfg.log_path, "logs") log_path: str = value_or(cfg.log_path, "logs")
packages_install_path: str = value_or(cfg.packages_install_path, "dbt_packages") packages_install_path: str = value_or(cfg.packages_install_path, "dbt_packages")
# in the default case we'll populate this once we know the adapter type # in the default case we'll populate this once we know the adapter type
# It would be nice to just pass along a Quoting here, but that would # It would be nice to just pass along a Quoting here, but that would
@@ -380,9 +369,6 @@ class PartialProject(RenderComponents):
snapshots: Dict[str, Any] snapshots: Dict[str, Any]
sources: Dict[str, Any] sources: Dict[str, Any]
tests: Dict[str, Any] tests: Dict[str, Any]
metrics: Dict[str, Any]
entities: Dict[str, Any]
exposures: Dict[str, Any]
vars_value: VarProvider vars_value: VarProvider
dispatch = cfg.dispatch dispatch = cfg.dispatch
@@ -391,9 +377,6 @@ class PartialProject(RenderComponents):
snapshots = cfg.snapshots snapshots = cfg.snapshots
sources = cfg.sources sources = cfg.sources
tests = cfg.tests tests = cfg.tests
metrics = cfg.metrics
entities = cfg.entities
exposures = cfg.exposures
if cfg.vars is None: if cfg.vars is None:
vars_dict: Dict[str, Any] = {} vars_dict: Dict[str, Any] = {}
else: else:
@@ -447,9 +430,6 @@ class PartialProject(RenderComponents):
query_comment=query_comment, query_comment=query_comment,
sources=sources, sources=sources,
tests=tests, tests=tests,
metrics=metrics,
entities=entities,
exposures=exposures,
vars=vars_value, vars=vars_value,
config_version=cfg.config_version, config_version=cfg.config_version,
unrendered=unrendered, unrendered=unrendered,
@@ -552,9 +532,6 @@ class Project:
snapshots: Dict[str, Any] snapshots: Dict[str, Any]
sources: Dict[str, Any] sources: Dict[str, Any]
tests: Dict[str, Any] tests: Dict[str, Any]
metrics: Dict[str, Any]
entities: Dict[str, Any]
exposures: Dict[str, Any]
vars: VarProvider vars: VarProvider
dbt_version: List[VersionSpecifier] dbt_version: List[VersionSpecifier]
packages: Dict[str, Any] packages: Dict[str, Any]
@@ -627,9 +604,6 @@ class Project:
"snapshots": self.snapshots, "snapshots": self.snapshots,
"sources": self.sources, "sources": self.sources,
"tests": self.tests, "tests": self.tests,
"metrics": self.metrics,
"entities": self.entities,
"exposures": self.exposures,
"vars": self.vars.to_dict(), "vars": self.vars.to_dict(),
"require-dbt-version": [v.to_version_string() for v in self.dbt_version], "require-dbt-version": [v.to_version_string() for v in self.dbt_version],
"config-version": self.config_version, "config-version": self.config_version,
@@ -647,7 +621,7 @@ class Project:
try: try:
ProjectContract.validate(self.to_project_config()) ProjectContract.validate(self.to_project_config())
except ValidationError as e: except ValidationError as e:
raise ProjectContractBrokenError(e) from e raise DbtProjectError(validator_error_message(e)) from e
@classmethod @classmethod
def partial_load(cls, project_root: str, *, verify_version: bool = False) -> PartialProject: def partial_load(cls, project_root: str, *, verify_version: bool = False) -> PartialProject:
@@ -672,8 +646,8 @@ class Project:
def get_selector(self, name: str) -> Union[SelectionSpec, bool]: def get_selector(self, name: str) -> Union[SelectionSpec, bool]:
if name not in self.selectors: if name not in self.selectors:
raise DbtRuntimeError( raise RuntimeException(
f"Could not find selector named {name}, expected one of {list(self.selectors)}" f"Could not find selector named {name}, expected one of " f"{list(self.selectors)}"
) )
return self.selectors[name]["definition"] return self.selectors[name]["definition"]

View File

@@ -1,14 +1,11 @@
from typing import Dict, Any, Tuple, Optional, Union, Callable from typing import Dict, Any, Tuple, Optional, Union, Callable
import re
import os
from dbt.clients.jinja import get_rendered, catch_jinja from dbt.clients.jinja import get_rendered, catch_jinja
from dbt.constants import SECRET_ENV_PREFIX
from dbt.context.target import TargetContext from dbt.context.target import TargetContext
from dbt.context.secret import SecretContext, SECRET_PLACEHOLDER from dbt.context.secret import SecretContext
from dbt.context.base import BaseContext from dbt.context.base import BaseContext
from dbt.contracts.connection import HasCredentials from dbt.contracts.connection import HasCredentials
from dbt.exceptions import DbtProjectError, CompilationError, RecursionError from dbt.exceptions import DbtProjectError, CompilationException, RecursionException
from dbt.utils import deep_map_render from dbt.utils import deep_map_render
@@ -40,14 +37,14 @@ class BaseRenderer:
try: try:
with catch_jinja(): with catch_jinja():
return get_rendered(value, self.context, native=True) return get_rendered(value, self.context, native=True)
except CompilationError as exc: except CompilationException as exc:
msg = f"Could not render {value}: {exc.msg}" msg = f"Could not render {value}: {exc.msg}"
raise CompilationError(msg) from exc raise CompilationException(msg) from exc
def render_data(self, data: Dict[str, Any]) -> Dict[str, Any]: def render_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
try: try:
return deep_map_render(self.render_entry, data) return deep_map_render(self.render_entry, data)
except RecursionError: except RecursionException:
raise DbtProjectError( raise DbtProjectError(
f"Cycle detected: {self.name} input has a reference to itself", project=data f"Cycle detected: {self.name} input has a reference to itself", project=data
) )
@@ -159,8 +156,7 @@ class DbtProjectYamlRenderer(BaseRenderer):
if first in {"seeds", "models", "snapshots", "tests"}: if first in {"seeds", "models", "snapshots", "tests"}:
keypath_parts = {(k.lstrip("+ ") if isinstance(k, str) else k) for k in keypath} keypath_parts = {(k.lstrip("+ ") if isinstance(k, str) else k) for k in keypath}
# model-level hooks # model-level hooks
late_rendered_hooks = {"pre-hook", "post-hook", "pre_hook", "post_hook"} if "pre-hook" in keypath_parts or "post-hook" in keypath_parts:
if keypath_parts.intersection(late_rendered_hooks):
return False return False
return True return True
@@ -178,28 +174,6 @@ class SecretRenderer(BaseRenderer):
def name(self): def name(self):
return "Secret" return "Secret"
def render_value(self, value: Any, keypath: Optional[Keypath] = None) -> Any:
# First, standard Jinja rendering, with special handling for 'secret' environment variables
# "{{ env_var('DBT_SECRET_ENV_VAR') }}" -> "$$$DBT_SECRET_START$$$DBT_SECRET_ENV_{VARIABLE_NAME}$$$DBT_SECRET_END$$$"
# This prevents Jinja manipulation of secrets via macros/filters that might leak partial/modified values in logs
rendered = super().render_value(value, keypath)
# Now, detect instances of the placeholder value ($$$DBT_SECRET_START...DBT_SECRET_END$$$)
# and replace them with the actual secret value
if SECRET_ENV_PREFIX in str(rendered):
search_group = f"({SECRET_ENV_PREFIX}(.*))"
pattern = SECRET_PLACEHOLDER.format(search_group).replace("$", r"\$")
m = re.search(
pattern,
rendered,
)
if m:
found = m.group(1)
value = os.environ[found]
replace_this = SECRET_PLACEHOLDER.format(found)
return rendered.replace(replace_this, value)
else:
return rendered
class ProfileRenderer(SecretRenderer): class ProfileRenderer(SecretRenderer):
@property @property

View File

@@ -1,44 +1,33 @@
import itertools import itertools
import os import os
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass, field from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import ( from typing import Dict, Any, Optional, Mapping, Iterator, Iterable, Tuple, List, MutableSet, Type
Any,
Dict,
Iterable,
Iterator,
Mapping,
MutableSet,
Optional,
Tuple,
Type,
Union,
)
from .profile import Profile
from .project import Project
from .renderer import DbtProjectYamlRenderer, ProfileRenderer
from .utils import parse_cli_vars
from dbt import flags from dbt import flags
from dbt.adapters.factory import get_include_paths, get_relation_class_by_name from dbt.adapters.factory import get_relation_class_by_name, get_include_paths
from dbt.helper_types import FQNPath, PathSet, DictDefaultEmptyStr
from dbt.config.profile import read_user_config from dbt.config.profile import read_user_config
from dbt.contracts.connection import AdapterRequiredConfig, Credentials from dbt.contracts.connection import AdapterRequiredConfig, Credentials
from dbt.contracts.graph.manifest import ManifestMetadata from dbt.contracts.graph.manifest import ManifestMetadata
from dbt.contracts.project import Configuration, UserConfig
from dbt.contracts.relation import ComponentName from dbt.contracts.relation import ComponentName
from dbt.dataclass_schema import ValidationError from dbt.ui import warning_tag
from dbt.exceptions import (
ConfigContractBrokenError,
DbtProjectError,
NonUniquePackageNameError,
DbtRuntimeError,
UninstalledPackagesFoundError,
)
from dbt.events.functions import warn_or_error
from dbt.events.types import UnusedResourceConfigPath
from dbt.helper_types import DictDefaultEmptyStr, FQNPath, PathSet
from .profile import Profile from dbt.contracts.project import Configuration, UserConfig
from .project import Project, PartialProject from dbt.exceptions import (
from .renderer import DbtProjectYamlRenderer, ProfileRenderer RuntimeException,
from .utils import parse_cli_vars DbtProjectError,
validator_error_message,
warn_or_error,
raise_compiler_error,
)
from dbt.dataclass_schema import ValidationError
def _project_quoting_dict(proj: Project, profile: Profile) -> Dict[ComponentName, bool]: def _project_quoting_dict(proj: Project, profile: Profile) -> Dict[ComponentName, bool]:
@@ -116,9 +105,6 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
query_comment=project.query_comment, query_comment=project.query_comment,
sources=project.sources, sources=project.sources,
tests=project.tests, tests=project.tests,
metrics=project.metrics,
entities=project.entities,
exposures=project.exposures,
vars=project.vars, vars=project.vars,
config_version=project.config_version, config_version=project.config_version,
unrendered=project.unrendered, unrendered=project.unrendered,
@@ -188,7 +174,7 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
try: try:
Configuration.validate(self.serialize()) Configuration.validate(self.serialize())
except ValidationError as e: except ValidationError as e:
raise ConfigContractBrokenError(e) from e raise DbtProjectError(validator_error_message(e)) from e
@classmethod @classmethod
def _get_rendered_profile( def _get_rendered_profile(
@@ -202,52 +188,28 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
@classmethod @classmethod
def collect_parts(cls: Type["RuntimeConfig"], args: Any) -> Tuple[Project, Profile]: def collect_parts(cls: Type["RuntimeConfig"], args: Any) -> Tuple[Project, Profile]:
# profile_name from the project
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, "vars", "{}"))
profile = cls.collect_profile(args=args)
project_renderer = DbtProjectYamlRenderer(profile, cli_vars)
project = cls.collect_project(args=args, project_renderer=project_renderer)
assert type(project) is Project
return (project, profile)
@classmethod
def collect_profile(
cls: Type["RuntimeConfig"], args: Any, profile_name: Optional[str] = None
) -> Profile:
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, "vars", "{}"))
profile_renderer = ProfileRenderer(cli_vars)
# build the profile using the base renderer and the one fact we know
if profile_name is None:
# Note: only the named profile section is rendered here. The rest of the
# profile is ignored.
partial = cls.collect_project(args)
assert type(partial) is PartialProject
profile_name = partial.render_profile_name(profile_renderer)
profile = cls._get_rendered_profile(args, profile_renderer, profile_name)
# Save env_vars encountered in rendering for partial parsing
profile.profile_env_vars = profile_renderer.ctx_obj.env_vars
return profile
@classmethod
def collect_project(
cls: Type["RuntimeConfig"],
args: Any,
project_renderer: Optional[DbtProjectYamlRenderer] = None,
) -> Union[Project, PartialProject]:
project_root = args.project_dir if args.project_dir else os.getcwd() project_root = args.project_dir if args.project_dir else os.getcwd()
version_check = bool(flags.VERSION_CHECK) version_check = bool(flags.VERSION_CHECK)
partial = Project.partial_load(project_root, verify_version=version_check) partial = Project.partial_load(project_root, verify_version=version_check)
if project_renderer is None:
return partial # build the profile using the base renderer and the one fact we know
else: # Note: only the named profile section is rendered. The rest of the
# profile is ignored.
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, "vars", "{}"))
profile_renderer = ProfileRenderer(cli_vars)
profile_name = partial.render_profile_name(profile_renderer)
profile = cls._get_rendered_profile(args, profile_renderer, profile_name)
# Save env_vars encountered in rendering for partial parsing
profile.profile_env_vars = profile_renderer.ctx_obj.env_vars
# get a new renderer using our target information and render the
# project
project_renderer = DbtProjectYamlRenderer(profile, cli_vars)
project = partial.render(project_renderer) project = partial.render(project_renderer)
# Save env_vars encountered in rendering for partial parsing
project.project_env_vars = project_renderer.ctx_obj.env_vars project.project_env_vars = project_renderer.ctx_obj.env_vars
return project return (project, profile)
# Called in main.py, lib.py, task/base.py # Called in main.py, lib.py, task/base.py
@classmethod @classmethod
@@ -259,7 +221,7 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
:param args: The arguments as parsed from the cli. :param args: The arguments as parsed from the cli.
:raises DbtProjectError: If the project is invalid or missing. :raises DbtProjectError: If the project is invalid or missing.
:raises DbtProfileError: If the profile is invalid or missing. :raises DbtProfileError: If the profile is invalid or missing.
:raises DbtValidationError: If the cli variables are invalid. :raises ValidationException: If the cli variables are invalid.
""" """
project, profile = cls.collect_parts(args) project, profile = cls.collect_parts(args)
@@ -312,16 +274,13 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
"snapshots": self._get_config_paths(self.snapshots), "snapshots": self._get_config_paths(self.snapshots),
"sources": self._get_config_paths(self.sources), "sources": self._get_config_paths(self.sources),
"tests": self._get_config_paths(self.tests), "tests": self._get_config_paths(self.tests),
"metrics": self._get_config_paths(self.metrics),
"entities": self._get_config_paths(self.entities),
"exposures": self._get_config_paths(self.exposures),
} }
def warn_for_unused_resource_config_paths( def get_unused_resource_config_paths(
self, self,
resource_fqns: Mapping[str, PathSet], resource_fqns: Mapping[str, PathSet],
disabled: PathSet, disabled: PathSet,
) -> None: ) -> List[FQNPath]:
"""Return a list of lists of strings, where each inner list of strings """Return a list of lists of strings, where each inner list of strings
represents a type + FQN path of a resource configuration that is not represents a type + FQN path of a resource configuration that is not
used. used.
@@ -335,13 +294,23 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
for config_path in config_paths: for config_path in config_paths:
if not _is_config_used(config_path, fqns): if not _is_config_used(config_path, fqns):
resource_path = ".".join(i for i in ((resource_type,) + config_path)) unused_resource_config_paths.append((resource_type,) + config_path)
unused_resource_config_paths.append(resource_path) return unused_resource_config_paths
if len(unused_resource_config_paths) == 0: def warn_for_unused_resource_config_paths(
self,
resource_fqns: Mapping[str, PathSet],
disabled: PathSet,
) -> None:
unused = self.get_unused_resource_config_paths(resource_fqns, disabled)
if len(unused) == 0:
return return
warn_or_error(UnusedResourceConfigPath(unused_config_paths=unused_resource_config_paths)) msg = UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE.format(
len(unused), "\n".join("- {}".format(".".join(u)) for u in unused)
)
warn_or_error(msg, log_fmt=warning_tag("{}"))
def load_dependencies(self, base_only=False) -> Mapping[str, "RuntimeConfig"]: def load_dependencies(self, base_only=False) -> Mapping[str, "RuntimeConfig"]:
if self.dependencies is None: if self.dependencies is None:
@@ -355,15 +324,22 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
count_packages_specified = len(self.packages.packages) # type: ignore count_packages_specified = len(self.packages.packages) # type: ignore
count_packages_installed = len(tuple(self._get_project_directories())) count_packages_installed = len(tuple(self._get_project_directories()))
if count_packages_specified > count_packages_installed: if count_packages_specified > count_packages_installed:
raise UninstalledPackagesFoundError( raise_compiler_error(
count_packages_specified, f"dbt found {count_packages_specified} package(s) "
count_packages_installed, f"specified in packages.yml, but only "
self.packages_install_path, f"{count_packages_installed} package(s) installed "
f'in {self.packages_install_path}. Run "dbt deps" to '
f"install package dependencies."
) )
project_paths = itertools.chain(internal_packages, self._get_project_directories()) project_paths = itertools.chain(internal_packages, self._get_project_directories())
for project_name, project in self.load_projects(project_paths): for project_name, project in self.load_projects(project_paths):
if project_name in all_projects: if project_name in all_projects:
raise NonUniquePackageNameError(project_name) raise_compiler_error(
f"dbt found more than one package with the name "
f'"{project_name}" included in this project. Package '
f"names must be unique in a project. Please rename "
f"one of these packages."
)
all_projects[project_name] = project all_projects[project_name] = project
self.dependencies = all_projects self.dependencies = all_projects
return self.dependencies return self.dependencies
@@ -428,7 +404,7 @@ class UnsetProfile(Profile):
def __getattribute__(self, name): def __getattribute__(self, name):
if name in {"profile_name", "target_name", "threads"}: if name in {"profile_name", "target_name", "threads"}:
raise DbtRuntimeError(f'Error: disallowed attribute "{name}" - no profile!') raise RuntimeException(f'Error: disallowed attribute "{name}" - no profile!')
return Profile.__getattribute__(self, name) return Profile.__getattribute__(self, name)
@@ -441,9 +417,6 @@ class UnsetProfileConfig(RuntimeConfig):
missing, any access to profile members results in an exception. missing, any access to profile members results in an exception.
""" """
profile_name: str = field(repr=False)
target_name: str = field(repr=False)
def __post_init__(self): def __post_init__(self):
# instead of futzing with InitVar overrides or rewriting __init__, just # instead of futzing with InitVar overrides or rewriting __init__, just
# `del` the attrs we don't want users touching. # `del` the attrs we don't want users touching.
@@ -455,7 +428,7 @@ class UnsetProfileConfig(RuntimeConfig):
def __getattribute__(self, name): def __getattribute__(self, name):
# Override __getattribute__ to check that the attribute isn't 'banned'. # Override __getattribute__ to check that the attribute isn't 'banned'.
if name in {"profile_name", "target_name"}: if name in {"profile_name", "target_name"}:
raise DbtRuntimeError(f'Error: disallowed attribute "{name}" - no profile!') raise RuntimeException(f'Error: disallowed attribute "{name}" - no profile!')
# avoid every attribute access triggering infinite recursion # avoid every attribute access triggering infinite recursion
return RuntimeConfig.__getattribute__(self, name) return RuntimeConfig.__getattribute__(self, name)
@@ -464,59 +437,6 @@ class UnsetProfileConfig(RuntimeConfig):
# re-override the poisoned profile behavior # re-override the poisoned profile behavior
return DictDefaultEmptyStr({}) return DictDefaultEmptyStr({})
def to_project_config(self, with_packages=False):
"""Return a dict representation of the config that could be written to
disk with `yaml.safe_dump` to get this configuration.
Overrides dbt.config.Project.to_project_config to omit undefined profile
attributes.
:param with_packages bool: If True, include the serialized packages
file in the root.
:returns dict: The serialized profile.
"""
result = deepcopy(
{
"name": self.project_name,
"version": self.version,
"project-root": self.project_root,
"profile": "",
"model-paths": self.model_paths,
"macro-paths": self.macro_paths,
"seed-paths": self.seed_paths,
"test-paths": self.test_paths,
"analysis-paths": self.analysis_paths,
"docs-paths": self.docs_paths,
"asset-paths": self.asset_paths,
"target-path": self.target_path,
"snapshot-paths": self.snapshot_paths,
"clean-targets": self.clean_targets,
"log-path": self.log_path,
"quoting": self.quoting,
"models": self.models,
"on-run-start": self.on_run_start,
"on-run-end": self.on_run_end,
"dispatch": self.dispatch,
"seeds": self.seeds,
"snapshots": self.snapshots,
"sources": self.sources,
"tests": self.tests,
"metrics": self.metrics,
"entities": self.entities,
"exposures": self.exposures,
"vars": self.vars.to_dict(),
"require-dbt-version": [v.to_version_string() for v in self.dbt_version],
"config-version": self.config_version,
}
)
if self.query_comment:
result["query-comment"] = self.query_comment.to_dict(omit_none=True)
if with_packages:
result.update(self.packages.to_dict(omit_none=True))
return result
@classmethod @classmethod
def from_parts( def from_parts(
cls, cls,
@@ -564,9 +484,6 @@ class UnsetProfileConfig(RuntimeConfig):
query_comment=project.query_comment, query_comment=project.query_comment,
sources=project.sources, sources=project.sources,
tests=project.tests, tests=project.tests,
metrics=project.metrics,
entities=project.entities,
exposures=project.exposures,
vars=project.vars, vars=project.vars,
config_version=project.config_version, config_version=project.config_version,
unrendered=project.unrendered, unrendered=project.unrendered,
@@ -606,13 +523,21 @@ class UnsetProfileConfig(RuntimeConfig):
:param args: The arguments as parsed from the cli. :param args: The arguments as parsed from the cli.
:raises DbtProjectError: If the project is invalid or missing. :raises DbtProjectError: If the project is invalid or missing.
:raises DbtProfileError: If the profile is invalid or missing. :raises DbtProfileError: If the profile is invalid or missing.
:raises DbtValidationError: If the cli variables are invalid. :raises ValidationException: If the cli variables are invalid.
""" """
project, profile = cls.collect_parts(args) project, profile = cls.collect_parts(args)
return cls.from_parts(project=project, profile=profile, args=args) return cls.from_parts(project=project, profile=profile, args=args)
UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE = """\
Configuration paths exist in your dbt_project.yml file which do not \
apply to any resources.
There are {} unused configuration paths:
{}
"""
def _is_config_used(path, fqns): def _is_config_used(path, fqns):
if fqns: if fqns:
for fqn in fqns: for fqn in fqns:

View File

@@ -1,5 +1,4 @@
from pathlib import Path from pathlib import Path
from copy import deepcopy
from typing import Dict, Any, Union from typing import Dict, Any, Union
from dbt.clients.yaml_helper import yaml, Loader, Dumper, load_yaml_text # noqa: F401 from dbt.clients.yaml_helper import yaml, Loader, Dumper, load_yaml_text # noqa: F401
from dbt.dataclass_schema import ValidationError from dbt.dataclass_schema import ValidationError
@@ -12,7 +11,7 @@ from dbt.clients.system import (
resolve_path_from_base, resolve_path_from_base,
) )
from dbt.contracts.selection import SelectorFile from dbt.contracts.selection import SelectorFile
from dbt.exceptions import DbtSelectorsError, DbtRuntimeError from dbt.exceptions import DbtSelectorsError, RuntimeException
from dbt.graph import parse_from_selectors_definition, SelectionSpec from dbt.graph import parse_from_selectors_definition, SelectionSpec
from dbt.graph.selector_spec import SelectionCriteria from dbt.graph.selector_spec import SelectionCriteria
@@ -46,7 +45,7 @@ class SelectorConfig(Dict[str, Dict[str, Union[SelectionSpec, bool]]]):
f"yaml-selectors", f"yaml-selectors",
result_type="invalid_selector", result_type="invalid_selector",
) from exc ) from exc
except DbtRuntimeError as exc: except RuntimeException as exc:
raise DbtSelectorsError( raise DbtSelectorsError(
f"Could not read selector file data: {exc}", f"Could not read selector file data: {exc}",
result_type="invalid_selector", result_type="invalid_selector",
@@ -62,7 +61,7 @@ class SelectorConfig(Dict[str, Dict[str, Union[SelectionSpec, bool]]]):
) -> "SelectorConfig": ) -> "SelectorConfig":
try: try:
rendered = renderer.render_data(data) rendered = renderer.render_data(data)
except (ValidationError, DbtRuntimeError) as exc: except (ValidationError, RuntimeException) as exc:
raise DbtSelectorsError( raise DbtSelectorsError(
f"Could not render selector data: {exc}", f"Could not render selector data: {exc}",
result_type="invalid_selector", result_type="invalid_selector",
@@ -77,7 +76,7 @@ class SelectorConfig(Dict[str, Dict[str, Union[SelectionSpec, bool]]]):
) -> "SelectorConfig": ) -> "SelectorConfig":
try: try:
data = load_yaml_text(load_file_contents(str(path))) data = load_yaml_text(load_file_contents(str(path)))
except (ValidationError, DbtRuntimeError) as exc: except (ValidationError, RuntimeException) as exc:
raise DbtSelectorsError( raise DbtSelectorsError(
f"Could not read selector file: {exc}", f"Could not read selector file: {exc}",
result_type="invalid_selector", result_type="invalid_selector",
@@ -141,33 +140,28 @@ def validate_selector_default(selector_file: SelectorFile) -> None:
# good to combine the two flows into one at some point. # good to combine the two flows into one at some point.
class SelectorDict: class SelectorDict:
@classmethod @classmethod
def parse_dict_definition(cls, definition, selector_dict={}): def parse_dict_definition(cls, definition):
key = list(definition)[0] key = list(definition)[0]
value = definition[key] value = definition[key]
if isinstance(value, list): if isinstance(value, list):
new_values = [] new_values = []
for sel_def in value: for sel_def in value:
new_value = cls.parse_from_definition(sel_def, selector_dict=selector_dict) new_value = cls.parse_from_definition(sel_def)
new_values.append(new_value) new_values.append(new_value)
value = new_values value = new_values
if key == "exclude": if key == "exclude":
definition = {key: value} definition = {key: value}
elif len(definition) == 1: elif len(definition) == 1:
definition = {"method": key, "value": value} definition = {"method": key, "value": value}
elif key == "method" and value == "selector":
sel_def = definition.get("value")
if sel_def not in selector_dict:
raise DbtSelectorsError(f"Existing selector definition for {sel_def} not found.")
return selector_dict[definition["value"]]["definition"]
return definition return definition
@classmethod @classmethod
def parse_a_definition(cls, def_type, definition, selector_dict={}): def parse_a_definition(cls, def_type, definition):
# this definition must be a list # this definition must be a list
new_dict = {def_type: []} new_dict = {def_type: []}
for sel_def in definition[def_type]: for sel_def in definition[def_type]:
if isinstance(sel_def, dict): if isinstance(sel_def, dict):
sel_def = cls.parse_from_definition(sel_def, selector_dict=selector_dict) sel_def = cls.parse_from_definition(sel_def)
new_dict[def_type].append(sel_def) new_dict[def_type].append(sel_def)
elif isinstance(sel_def, str): elif isinstance(sel_def, str):
sel_def = SelectionCriteria.dict_from_single_spec(sel_def) sel_def = SelectionCriteria.dict_from_single_spec(sel_def)
@@ -177,17 +171,15 @@ class SelectorDict:
return new_dict return new_dict
@classmethod @classmethod
def parse_from_definition(cls, definition, selector_dict={}): def parse_from_definition(cls, definition):
if isinstance(definition, str): if isinstance(definition, str):
definition = SelectionCriteria.dict_from_single_spec(definition) definition = SelectionCriteria.dict_from_single_spec(definition)
elif "union" in definition: elif "union" in definition:
definition = cls.parse_a_definition("union", definition, selector_dict=selector_dict) definition = cls.parse_a_definition("union", definition)
elif "intersection" in definition: elif "intersection" in definition:
definition = cls.parse_a_definition( definition = cls.parse_a_definition("intersection", definition)
"intersection", definition, selector_dict=selector_dict
)
elif isinstance(definition, dict): elif isinstance(definition, dict):
definition = cls.parse_dict_definition(definition, selector_dict=selector_dict) definition = cls.parse_dict_definition(definition)
return definition return definition
# This is the normal entrypoint of this code. Give it the # This is the normal entrypoint of this code. Give it the
@@ -198,8 +190,6 @@ class SelectorDict:
for selector in selectors: for selector in selectors:
sel_name = selector["name"] sel_name = selector["name"]
selector_dict[sel_name] = selector selector_dict[sel_name] = selector
definition = cls.parse_from_definition( definition = cls.parse_from_definition(selector["definition"])
selector["definition"], selector_dict=deepcopy(selector_dict)
)
selector_dict[sel_name]["definition"] = definition selector_dict[sel_name]["definition"] = definition
return selector_dict return selector_dict

View File

@@ -8,24 +8,24 @@ from dbt.clients import yaml_helper
from dbt.config import Profile, Project, read_user_config from dbt.config import Profile, Project, read_user_config
from dbt.config.renderer import DbtProjectYamlRenderer, ProfileRenderer from dbt.config.renderer import DbtProjectYamlRenderer, ProfileRenderer
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import InvalidOptionYAML from dbt.events.types import InvalidVarsYAML
from dbt.exceptions import DbtValidationError, OptionNotYamlDictError from dbt.exceptions import ValidationException, raise_compiler_error
def parse_cli_vars(var_string: str) -> Dict[str, Any]: def parse_cli_vars(var_string: str) -> Dict[str, Any]:
return parse_cli_yaml_string(var_string, "vars")
def parse_cli_yaml_string(var_string: str, cli_option_name: str) -> Dict[str, Any]:
try: try:
cli_vars = yaml_helper.load_yaml_text(var_string) cli_vars = yaml_helper.load_yaml_text(var_string)
var_type = type(cli_vars) var_type = type(cli_vars)
if var_type is dict: if var_type is dict:
return cli_vars return cli_vars
else: else:
raise OptionNotYamlDictError(var_type, cli_option_name) type_name = var_type.__name__
except DbtValidationError: raise_compiler_error(
fire_event(InvalidOptionYAML(option_name=cli_option_name)) "The --vars argument must be a YAML dictionary, but was "
"of type '{}'".format(type_name)
)
except ValidationException:
fire_event(InvalidVarsYAML())
raise raise

View File

@@ -1,10 +0,0 @@
SECRET_ENV_PREFIX = "DBT_ENV_SECRET_"
DEFAULT_ENV_PLACEHOLDER = "DBT_DEFAULT_PLACEHOLDER"
METADATA_ENV_PREFIX = "DBT_ENV_CUSTOM_ENV_"
MAXIMUM_SEED_SIZE = 1 * 1024 * 1024
MAXIMUM_SEED_SIZE_NAME = "1MB"
PIN_PACKAGE_URL = (
"https://docs.getdbt.com/docs/package-management#section-specifying-package-versions"
)

View File

@@ -1,51 +1 @@
# Contexts and Jinja rendering # Contexts and Jinja rendering
Contexts are used for Jinja rendering. They include context methods, executable macros, and various settings that are available in Jinja.
The most common entrypoint to Jinja rendering in dbt is a method named `get_rendered`, which takes two arguments: templated code (string), and a context used to render it (dictionary).
The context is the bundle of information that is in "scope" when rendering Jinja-templated code. For instance, imagine a simple Jinja template:
```
{% set new_value = some_macro(some_variable) %}
```
Both `some_macro()` and `some_variable` must be defined in that context. Otherwise, it will raise an error when rendering.
Different contexts are used in different places because we allow access to different methods and data in different places. Executable SQL, for example, includes all available macros and the model being run. The variables and macros in scope for Jinja defined in yaml files is much more limited.
### Implementation
The context that is passed to Jinja is always in a dictionary format, not an actual class, so a `to_dict()` is executed on a context class before it is used for rendering.
Each context has a `generate_<name>_context` function to create the context. `ProviderContext` subclasses have different generate functions for parsing and for execution, so that certain functions (notably `ref`, `source`, and `config`) can return different results
### Hierarchy
All contexts inherit from the `BaseContext`, which includes "pure" methods (e.g. `tojson`), `env_var()`, and `var()` (but only CLI values, passed via `--vars`).
Methods available in parent contexts are also available in child contexts.
```
BaseContext -- core/dbt/context/base.py
SecretContext -- core/dbt/context/secret.py
TargetContext -- core/dbt/context/target.py
ConfiguredContext -- core/dbt/context/configured.py
SchemaYamlContext -- core/dbt/context/configured.py
DocsRuntimeContext -- core/dbt/context/configured.py
MacroResolvingContext -- core/dbt/context/configured.py
ManifestContext -- core/dbt/context/manifest.py
QueryHeaderContext -- core/dbt/context/manifest.py
ProviderContext -- core/dbt/context/provider.py
MacroContext -- core/dbt/context/provider.py
ModelContext -- core/dbt/context/provider.py
TestContext -- core/dbt/context/provider.py
```
### Contexts for configuration
Contexts for rendering "special" `.yml` (configuration) files:
- `SecretContext`: Supports "secret" env vars, which are prefixed with `DBT_ENV_SECRET_`. Used for rendering in `profiles.yml` and `packages.yml` ONLY. Secrets defined elsewhere will raise explicit errors.
- `TargetContext`: The same as `Base`, plus `target` (connection profile). Used most notably in `dbt_project.yml` and `selectors.yml`.
Contexts for other `.yml` files in the project:
- `SchemaYamlContext`: Supports `vars` declared on the CLI and in `dbt_project.yml`. Does not support custom macros, beyond `var()` + `env_var()` methods. Used for all `.yml` files, to define properties and configuration.
- `DocsRuntimeContext`: Standard `.yml` file context, plus `doc()` method (with all `docs` blocks in scope). Used to resolve `description` properties.

View File

@@ -1,25 +1,21 @@
import json import json
import os import os
from typing import Any, Dict, NoReturn, Optional, Mapping, Iterable, Set, List from typing import Any, Dict, NoReturn, Optional, Mapping
from dbt import flags from dbt import flags
from dbt import tracking from dbt import tracking
from dbt import utils
from dbt.clients.jinja import get_rendered from dbt.clients.jinja import get_rendered
from dbt.clients.yaml_helper import yaml, safe_load, SafeLoader, Loader, Dumper # noqa: F401 from dbt.clients.yaml_helper import yaml, safe_load, SafeLoader, Loader, Dumper # noqa: F401
from dbt.constants import SECRET_ENV_PREFIX, DEFAULT_ENV_PLACEHOLDER from dbt.contracts.graph.compiled import CompiledResource
from dbt.contracts.graph.nodes import Resource
from dbt.exceptions import ( from dbt.exceptions import (
SecretEnvVarLocationError, raise_compiler_error,
EnvVarMissingError,
MacroReturn, MacroReturn,
RequiredVarNotFoundError, raise_parsing_error,
SetStrictWrongTypeError, disallow_secret_env_var,
ZipStrictWrongTypeError,
) )
from dbt.logger import SECRET_ENV_PREFIX
from dbt.events.functions import fire_event, get_invocation_id from dbt.events.functions import fire_event, get_invocation_id
from dbt.events.types import JinjaLogInfo, JinjaLogDebug from dbt.events.types import MacroEventInfo, MacroEventDebug
from dbt.events.contextvars import get_node_info
from dbt.version import __version__ as dbt_version from dbt.version import __version__ as dbt_version
# These modules are added to the context. Consider alternative # These modules are added to the context. Consider alternative
@@ -27,9 +23,39 @@ from dbt.version import __version__ as dbt_version
import pytz import pytz
import datetime import datetime
import re import re
import itertools
# See the `contexts` module README for more information on how contexts work # Contexts in dbt Core
# Contexts are used for Jinja rendering. They include context methods,
# executable macros, and various settings that are available in Jinja.
#
# Different contexts are used in different places because we allow access
# to different methods and data in different places. Executable SQL, for
# example, includes the available macros and the model, while Jinja in
# yaml files is more limited.
#
# The context that is passed to Jinja is always in a dictionary format,
# not an actual class, so a 'to_dict()' is executed on a context class
# before it is used for rendering.
#
# Each context has a generate_<name>_context function to create the context.
# ProviderContext subclasses have different generate functions for
# parsing and for execution.
#
# Context class hierarchy
#
# BaseContext -- core/dbt/context/base.py
# SecretContext -- core/dbt/context/secret.py
# TargetContext -- core/dbt/context/target.py
# ConfiguredContext -- core/dbt/context/configured.py
# SchemaYamlContext -- core/dbt/context/configured.py
# DocsRuntimeContext -- core/dbt/context/configured.py
# MacroResolvingContext -- core/dbt/context/configured.py
# ManifestContext -- core/dbt/context/manifest.py
# QueryHeaderContext -- core/dbt/context/manifest.py
# ProviderContext -- core/dbt/context/provider.py
# MacroContext -- core/dbt/context/provider.py
# ModelContext -- core/dbt/context/provider.py
# TestContext -- core/dbt/context/provider.py
def get_pytz_module_context() -> Dict[str, Any]: def get_pytz_module_context() -> Dict[str, Any]:
@@ -51,35 +77,11 @@ def get_re_module_context() -> Dict[str, Any]:
return {name: getattr(re, name) for name in context_exports} return {name: getattr(re, name) for name in context_exports}
def get_itertools_module_context() -> Dict[str, Any]:
# Excluded dropwhile, filterfalse, takewhile and groupby;
# first 3 illogical for Jinja and last redundant.
context_exports = [
"count",
"cycle",
"repeat",
"accumulate",
"chain",
"compress",
"islice",
"starmap",
"tee",
"zip_longest",
"product",
"permutations",
"combinations",
"combinations_with_replacement",
]
return {name: getattr(itertools, name) for name in context_exports}
def get_context_modules() -> Dict[str, Dict[str, Any]]: def get_context_modules() -> Dict[str, Dict[str, Any]]:
return { return {
"pytz": get_pytz_module_context(), "pytz": get_pytz_module_context(),
"datetime": get_datetime_module_context(), "datetime": get_datetime_module_context(),
"re": get_re_module_context(), "re": get_re_module_context(),
"itertools": get_itertools_module_context(),
} }
@@ -129,17 +131,18 @@ class ContextMeta(type):
class Var: class Var:
UndefinedVarError = "Required var '{}' not found in config:\nVars " "supplied to {} = {}"
_VAR_NOTSET = object() _VAR_NOTSET = object()
def __init__( def __init__(
self, self,
context: Mapping[str, Any], context: Mapping[str, Any],
cli_vars: Mapping[str, Any], cli_vars: Mapping[str, Any],
node: Optional[Resource] = None, node: Optional[CompiledResource] = None,
) -> None: ) -> None:
self._context: Mapping[str, Any] = context self._context: Mapping[str, Any] = context
self._cli_vars: Mapping[str, Any] = cli_vars self._cli_vars: Mapping[str, Any] = cli_vars
self._node: Optional[Resource] = node self._node: Optional[CompiledResource] = node
self._merged: Mapping[str, Any] = self._generate_merged() self._merged: Mapping[str, Any] = self._generate_merged()
def _generate_merged(self) -> Mapping[str, Any]: def _generate_merged(self) -> Mapping[str, Any]:
@@ -153,7 +156,10 @@ class Var:
return "<Configuration>" return "<Configuration>"
def get_missing_var(self, var_name): def get_missing_var(self, var_name):
raise RequiredVarNotFoundError(var_name, self._merged, self._node) dct = {k: self._merged[k] for k in self._merged}
pretty_vars = json.dumps(dct, sort_keys=True, indent=4)
msg = self.UndefinedVarError.format(var_name, self.node_name, pretty_vars)
raise_compiler_error(msg, self._node)
def has_var(self, var_name: str): def has_var(self, var_name: str):
return var_name in self._merged return var_name in self._merged
@@ -297,22 +303,18 @@ class BaseContext(metaclass=ContextMeta):
""" """
return_value = None return_value = None
if var.startswith(SECRET_ENV_PREFIX): if var.startswith(SECRET_ENV_PREFIX):
raise SecretEnvVarLocationError(var) disallow_secret_env_var(var)
if var in os.environ: if var in os.environ:
return_value = os.environ[var] return_value = os.environ[var]
elif default is not None: elif default is not None:
return_value = default return_value = default
if return_value is not None: if return_value is not None:
# If the environment variable is set from a default, store a string indicating self.env_vars[var] = return_value
# that so we can skip partial parsing. Otherwise the file will be scheduled for
# reparsing. If the default changes, the file will have been updated and therefore
# will be scheduled for reparsing anyways.
self.env_vars[var] = return_value if var in os.environ else DEFAULT_ENV_PLACEHOLDER
return return_value return return_value
else: else:
raise EnvVarMissingError(var) msg = f"Env var required but not provided: '{var}'"
raise_parsing_error(msg)
if os.environ.get("DBT_MACRO_DEBUGGING"): if os.environ.get("DBT_MACRO_DEBUGGING"):
@@ -455,90 +457,6 @@ class BaseContext(metaclass=ContextMeta):
except (ValueError, yaml.YAMLError): except (ValueError, yaml.YAMLError):
return default return default
@contextmember("set")
@staticmethod
def _set(value: Iterable[Any], default: Any = None) -> Optional[Set[Any]]:
"""The `set` context method can be used to convert any iterable
to a sequence of iterable elements that are unique (a set).
:param value: The iterable
:param default: A default value to return if the `value` argument
is not an iterable
Usage:
{% set my_list = [1, 2, 2, 3] %}
{% set my_set = set(my_list) %}
{% do log(my_set) %} {# {1, 2, 3} #}
"""
try:
return set(value)
except TypeError:
return default
@contextmember
@staticmethod
def set_strict(value: Iterable[Any]) -> Set[Any]:
"""The `set_strict` context method can be used to convert any iterable
to a sequence of iterable elements that are unique (a set). The
difference to the `set` context method is that the `set_strict` method
will raise an exception on a TypeError.
:param value: The iterable
Usage:
{% set my_list = [1, 2, 2, 3] %}
{% set my_set = set_strict(my_list) %}
{% do log(my_set) %} {# {1, 2, 3} #}
"""
try:
return set(value)
except TypeError as e:
raise SetStrictWrongTypeError(e)
@contextmember("zip")
@staticmethod
def _zip(*args: Iterable[Any], default: Any = None) -> Optional[Iterable[Any]]:
"""The `zip` context method can be used to used to return
an iterator of tuples, where the i-th tuple contains the i-th
element from each of the argument iterables.
:param *args: Any number of iterables
:param default: A default value to return if `*args` is not
iterable
Usage:
{% set my_list_a = [1, 2] %}
{% set my_list_b = ['alice', 'bob'] %}
{% set my_zip = zip(my_list_a, my_list_b) | list %}
{% do log(my_set) %} {# [(1, 'alice'), (2, 'bob')] #}
"""
try:
return zip(*args)
except TypeError:
return default
@contextmember
@staticmethod
def zip_strict(*args: Iterable[Any]) -> Iterable[Any]:
"""The `zip_strict` context method can be used to used to return
an iterator of tuples, where the i-th tuple contains the i-th
element from each of the argument iterables. The difference to the
`zip` context method is that the `zip_strict` method will raise an
exception on a TypeError.
:param *args: Any number of iterables
Usage:
{% set my_list_a = [1, 2] %}
{% set my_list_b = ['alice', 'bob'] %}
{% set my_zip = zip_strict(my_list_a, my_list_b) | list %}
{% do log(my_set) %} {# [(1, 'alice'), (2, 'bob')] #}
"""
try:
return zip(*args)
except TypeError as e:
raise ZipStrictWrongTypeError(e)
@contextmember @contextmember
@staticmethod @staticmethod
def log(msg: str, info: bool = False) -> str: def log(msg: str, info: bool = False) -> str:
@@ -555,9 +473,9 @@ class BaseContext(metaclass=ContextMeta):
{% endmacro %}" {% endmacro %}"
""" """
if info: if info:
fire_event(JinjaLogInfo(msg=msg, node_info=get_node_info())) fire_event(MacroEventInfo(msg=msg))
else: else:
fire_event(JinjaLogDebug(msg=msg, node_info=get_node_info())) fire_event(MacroEventDebug(msg=msg))
return "" return ""
@contextproperty @contextproperty
@@ -634,8 +552,9 @@ class BaseContext(metaclass=ContextMeta):
{% endif %} {% endif %}
This supports all flags defined in flags submodule (core/dbt/flags.py) This supports all flags defined in flags submodule (core/dbt/flags.py)
TODO: Replace with object that provides read-only access to flag values
""" """
return flags.get_flag_obj() return flags
@contextmember @contextmember
@staticmethod @staticmethod
@@ -655,48 +574,6 @@ class BaseContext(metaclass=ContextMeta):
print(msg) print(msg)
return "" return ""
@contextmember
@staticmethod
def diff_of_two_dicts(
dict_a: Dict[str, List[str]], dict_b: Dict[str, List[str]]
) -> Dict[str, List[str]]:
"""
Given two dictionaries of type Dict[str, List[str]]:
dict_a = {'key_x': ['value_1', 'VALUE_2'], 'KEY_Y': ['value_3']}
dict_b = {'key_x': ['value_1'], 'key_z': ['value_4']}
Return the same dictionary representation of dict_a MINUS dict_b,
performing a case-insensitive comparison between the strings in each.
All keys returned will be in the original case of dict_a.
returns {'key_x': ['VALUE_2'], 'KEY_Y': ['value_3']}
"""
dict_diff = {}
dict_b_lowered = {k.casefold(): [x.casefold() for x in v] for k, v in dict_b.items()}
for k in dict_a:
if k.casefold() in dict_b_lowered.keys():
diff = []
for v in dict_a[k]:
if v.casefold() not in dict_b_lowered[k.casefold()]:
diff.append(v)
if diff:
dict_diff.update({k: diff})
else:
dict_diff.update({k: dict_a[k]})
return dict_diff
@contextmember
@staticmethod
def local_md5(value: str) -> str:
"""Calculates an MD5 hash of the given string.
It's called "local_md5" to emphasize that it runs locally in dbt (in jinja context) and not an MD5 SQL command.
:param value: The value to hash
Usage:
{% set value_hash = local_md5("hello world") %}
"""
return utils.md5(value)
def generate_base_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]: def generate_base_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]:
ctx = BaseContext(cli_vars) ctx = BaseContext(cli_vars)

View File

@@ -1,14 +1,14 @@
import os import os
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from dbt.constants import SECRET_ENV_PREFIX, DEFAULT_ENV_PLACEHOLDER
from dbt.contracts.connection import AdapterRequiredConfig from dbt.contracts.connection import AdapterRequiredConfig
from dbt.logger import SECRET_ENV_PREFIX
from dbt.node_types import NodeType from dbt.node_types import NodeType
from dbt.utils import MultiDict from dbt.utils import MultiDict
from dbt.context.base import contextproperty, contextmember, Var from dbt.context.base import contextproperty, contextmember, Var
from dbt.context.target import TargetContext from dbt.context.target import TargetContext
from dbt.exceptions import EnvVarMissingError, SecretEnvVarLocationError from dbt.exceptions import raise_parsing_error, disallow_secret_env_var
class ConfiguredContext(TargetContext): class ConfiguredContext(TargetContext):
@@ -86,7 +86,7 @@ class SchemaYamlContext(ConfiguredContext):
def env_var(self, var: str, default: Optional[str] = None) -> str: def env_var(self, var: str, default: Optional[str] = None) -> str:
return_value = None return_value = None
if var.startswith(SECRET_ENV_PREFIX): if var.startswith(SECRET_ENV_PREFIX):
raise SecretEnvVarLocationError(var) disallow_secret_env_var(var)
if var in os.environ: if var in os.environ:
return_value = os.environ[var] return_value = os.environ[var]
elif default is not None: elif default is not None:
@@ -94,17 +94,11 @@ class SchemaYamlContext(ConfiguredContext):
if return_value is not None: if return_value is not None:
if self.schema_yaml_vars: if self.schema_yaml_vars:
# If the environment variable is set from a default, store a string indicating self.schema_yaml_vars.env_vars[var] = return_value
# that so we can skip partial parsing. Otherwise the file will be scheduled for
# reparsing. If the default changes, the file will have been updated and therefore
# will be scheduled for reparsing anyways.
self.schema_yaml_vars.env_vars[var] = (
return_value if var in os.environ else DEFAULT_ENV_PLACEHOLDER
)
return return_value return return_value
else: else:
raise EnvVarMissingError(var) msg = f"Env var required but not provided: '{var}'"
raise_parsing_error(msg)
class MacroResolvingContext(ConfiguredContext): class MacroResolvingContext(ConfiguredContext):

View File

@@ -4,8 +4,8 @@ from dataclasses import dataclass
from typing import List, Iterator, Dict, Any, TypeVar, Generic from typing import List, Iterator, Dict, Any, TypeVar, Generic
from dbt.config import RuntimeConfig, Project, IsFQNResource from dbt.config import RuntimeConfig, Project, IsFQNResource
from dbt.contracts.graph.model_config import BaseConfig, get_config_for, _listify from dbt.contracts.graph.model_config import BaseConfig, get_config_for
from dbt.exceptions import DbtInternalError from dbt.exceptions import InternalException
from dbt.node_types import NodeType from dbt.node_types import NodeType
from dbt.utils import fqn_search from dbt.utils import fqn_search
@@ -43,14 +43,9 @@ class UnrenderedConfig(ConfigSource):
model_configs = unrendered.get("sources") model_configs = unrendered.get("sources")
elif resource_type == NodeType.Test: elif resource_type == NodeType.Test:
model_configs = unrendered.get("tests") model_configs = unrendered.get("tests")
elif resource_type == NodeType.Metric:
model_configs = unrendered.get("metrics")
elif resource_type == NodeType.Entity:
model_configs = unrendered.get("entities")
elif resource_type == NodeType.Exposure:
model_configs = unrendered.get("exposures")
else: else:
model_configs = unrendered.get("models") model_configs = unrendered.get("models")
if model_configs is None: if model_configs is None:
return {} return {}
else: else:
@@ -70,12 +65,6 @@ class RenderedConfig(ConfigSource):
model_configs = self.project.sources model_configs = self.project.sources
elif resource_type == NodeType.Test: elif resource_type == NodeType.Test:
model_configs = self.project.tests model_configs = self.project.tests
elif resource_type == NodeType.Metric:
model_configs = self.project.metrics
elif resource_type == NodeType.Entity:
model_configs = self.project.entities
elif resource_type == NodeType.Exposure:
model_configs = self.project.exposures
else: else:
model_configs = self.project.models model_configs = self.project.models
return model_configs return model_configs
@@ -93,7 +82,7 @@ class BaseContextConfigGenerator(Generic[T]):
return self._active_project return self._active_project
dependencies = self._active_project.load_dependencies() dependencies = self._active_project.load_dependencies()
if project_name not in dependencies: if project_name not in dependencies:
raise DbtInternalError( raise InternalException(
f"Project name {project_name} not found in dependencies " f"Project name {project_name} not found in dependencies "
f"(found {list(dependencies)})" f"(found {list(dependencies)})"
) )
@@ -275,51 +264,20 @@ class ContextConfig:
@classmethod @classmethod
def _add_config_call(cls, config_call_dict, opts: Dict[str, Any]) -> None: def _add_config_call(cls, config_call_dict, opts: Dict[str, Any]) -> None:
# config_call_dict is already encountered configs, opts is new
# This mirrors code in _merge_field_value in model_config.py which is similar but
# operates on config objects.
for k, v in opts.items(): for k, v in opts.items():
# MergeBehavior for post-hook and pre-hook is to collect all # MergeBehavior for post-hook and pre-hook is to collect all
# values, instead of overwriting # values, instead of overwriting
if k in BaseConfig.mergebehavior["append"]: if k in BaseConfig.mergebehavior["append"]:
if not isinstance(v, list): if not isinstance(v, list):
v = [v] v = [v]
if k in config_call_dict: # should always be a list here if k in BaseConfig.mergebehavior["update"] and not isinstance(v, dict):
raise InternalException(f"expected dict, got {v}")
if k in config_call_dict and isinstance(config_call_dict[k], list):
config_call_dict[k].extend(v) config_call_dict[k].extend(v)
else: elif k in config_call_dict and isinstance(config_call_dict[k], dict):
config_call_dict[k] = v
elif k in BaseConfig.mergebehavior["update"]:
if not isinstance(v, dict):
raise DbtInternalError(f"expected dict, got {v}")
if k in config_call_dict and isinstance(config_call_dict[k], dict):
config_call_dict[k].update(v) config_call_dict[k].update(v)
else: else:
config_call_dict[k] = v config_call_dict[k] = v
elif k in BaseConfig.mergebehavior["dict_key_append"]:
if not isinstance(v, dict):
raise DbtInternalError(f"expected dict, got {v}")
if k in config_call_dict: # should always be a dict
for key, value in v.items():
extend = False
# This might start with a +, to indicate we should extend the list
# instead of just clobbering it
if key.startswith("+"):
extend = True
if key in config_call_dict[k] and extend:
# extend the list
config_call_dict[k][key].extend(_listify(value))
else:
# clobber the list
config_call_dict[k][key] = _listify(value)
else:
# This is always a dictionary
config_call_dict[k] = v
# listify everything
for key, value in config_call_dict[k].items():
config_call_dict[k][key] = _listify(value)
else:
config_call_dict[k] = v
def build_config_dict( def build_config_dict(
self, base: bool = False, *, rendered: bool = True, patch_config_dict: dict = None self, base: bool = False, *, rendered: bool = True, patch_config_dict: dict = None

View File

@@ -1,12 +1,13 @@
from typing import Any, Dict, Union from typing import Any, Dict, Union
from dbt.exceptions import ( from dbt.exceptions import (
DocTargetNotFoundError, doc_invalid_args,
DocArgsError, doc_target_not_found,
) )
from dbt.config.runtime import RuntimeConfig from dbt.config.runtime import RuntimeConfig
from dbt.contracts.graph.compiled import CompileResultNode
from dbt.contracts.graph.manifest import Manifest from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.graph.nodes import Macro, ResultNode from dbt.contracts.graph.parsed import ParsedMacro
from dbt.context.base import contextmember from dbt.context.base import contextmember
from dbt.context.configured import SchemaYamlContext from dbt.context.configured import SchemaYamlContext
@@ -16,7 +17,7 @@ class DocsRuntimeContext(SchemaYamlContext):
def __init__( def __init__(
self, self,
config: RuntimeConfig, config: RuntimeConfig,
node: Union[Macro, ResultNode], node: Union[ParsedMacro, CompileResultNode],
manifest: Manifest, manifest: Manifest,
current_project: str, current_project: str,
) -> None: ) -> None:
@@ -52,9 +53,9 @@ class DocsRuntimeContext(SchemaYamlContext):
elif len(args) == 2: elif len(args) == 2:
doc_package_name, doc_name = args doc_package_name, doc_name = args
else: else:
raise DocArgsError(self.node, args) doc_invalid_args(self.node, args)
# Documentation # ParsedDocumentation
target_doc = self.manifest.resolve_doc( target_doc = self.manifest.resolve_doc(
doc_name, doc_name,
doc_package_name, doc_package_name,
@@ -68,9 +69,7 @@ class DocsRuntimeContext(SchemaYamlContext):
# TODO CT-211 # TODO CT-211
source_file.add_node(self.node.unique_id) # type: ignore[union-attr] source_file.add_node(self.node.unique_id) # type: ignore[union-attr]
else: else:
raise DocTargetNotFoundError( doc_target_not_found(self.node, doc_name, doc_package_name)
node=self.node, target_doc_name=doc_name, target_doc_package=doc_package_name
)
return target_doc.block_contents return target_doc.block_contents

View File

@@ -1,144 +0,0 @@
import functools
from typing import NoReturn
from dbt.events.functions import warn_or_error
from dbt.events.helpers import env_secrets, scrub_secrets
from dbt.events.types import JinjaLogWarning
from dbt.exceptions import (
DbtRuntimeError,
MissingConfigError,
MissingMaterializationError,
MissingRelationError,
AmbiguousAliasError,
AmbiguousCatalogMatchError,
CacheInconsistencyError,
DataclassNotDictError,
CompilationError,
DbtDatabaseError,
DependencyNotFoundError,
DependencyError,
DuplicatePatchPathError,
DuplicateResourceNameError,
PropertyYMLError,
NotImplementedError,
RelationWrongTypeError,
)
def warn(msg, node=None):
warn_or_error(JinjaLogWarning(msg=msg), node=node)
return ""
def missing_config(model, name) -> NoReturn:
raise MissingConfigError(unique_id=model.unique_id, name=name)
def missing_materialization(model, adapter_type) -> NoReturn:
raise MissingMaterializationError(
materialization=model.config.materialized, adapter_type=adapter_type
)
def missing_relation(relation, model=None) -> NoReturn:
raise MissingRelationError(relation, model)
def raise_ambiguous_alias(node_1, node_2, duped_name=None) -> NoReturn:
raise AmbiguousAliasError(node_1, node_2, duped_name)
def raise_ambiguous_catalog_match(unique_id, match_1, match_2) -> NoReturn:
raise AmbiguousCatalogMatchError(unique_id, match_1, match_2)
def raise_cache_inconsistent(message) -> NoReturn:
raise CacheInconsistencyError(message)
def raise_dataclass_not_dict(obj) -> NoReturn:
raise DataclassNotDictError(obj)
def raise_compiler_error(msg, node=None) -> NoReturn:
raise CompilationError(msg, node)
def raise_database_error(msg, node=None) -> NoReturn:
raise DbtDatabaseError(msg, node)
def raise_dep_not_found(node, node_description, required_pkg) -> NoReturn:
raise DependencyNotFoundError(node, node_description, required_pkg)
def raise_dependency_error(msg) -> NoReturn:
raise DependencyError(scrub_secrets(msg, env_secrets()))
def raise_duplicate_patch_name(patch_1, existing_patch_path) -> NoReturn:
raise DuplicatePatchPathError(patch_1, existing_patch_path)
def raise_duplicate_resource_name(node_1, node_2) -> NoReturn:
raise DuplicateResourceNameError(node_1, node_2)
def raise_invalid_property_yml_version(path, issue) -> NoReturn:
raise PropertyYMLError(path, issue)
def raise_not_implemented(msg) -> NoReturn:
raise NotImplementedError(msg)
def relation_wrong_type(relation, expected_type, model=None) -> NoReturn:
raise RelationWrongTypeError(relation, expected_type, model)
# Update this when a new function should be added to the
# dbt context's `exceptions` key!
CONTEXT_EXPORTS = {
fn.__name__: fn
for fn in [
warn,
missing_config,
missing_materialization,
missing_relation,
raise_ambiguous_alias,
raise_ambiguous_catalog_match,
raise_cache_inconsistent,
raise_dataclass_not_dict,
raise_compiler_error,
raise_database_error,
raise_dep_not_found,
raise_dependency_error,
raise_duplicate_patch_name,
raise_duplicate_resource_name,
raise_invalid_property_yml_version,
raise_not_implemented,
relation_wrong_type,
]
}
# wraps context based exceptions in node info
def wrapper(model):
def wrap(func):
@functools.wraps(func)
def inner(*args, **kwargs):
try:
return func(*args, **kwargs)
except DbtRuntimeError as exc:
exc.add_node(model)
raise exc
return inner
return wrap
def wrapped_exports(model):
wrap = wrapper(model)
return {name: wrap(export) for name, export in CONTEXT_EXPORTS.items()}

View File

@@ -1,10 +1,10 @@
from typing import Dict, MutableMapping, Optional from typing import Dict, MutableMapping, Optional
from dbt.contracts.graph.nodes import Macro from dbt.contracts.graph.parsed import ParsedMacro
from dbt.exceptions import DuplicateMacroNameError, PackageNotFoundForMacroError from dbt.exceptions import raise_duplicate_macro_name, raise_compiler_error
from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
from dbt.clients.jinja import MacroGenerator from dbt.clients.jinja import MacroGenerator
MacroNamespace = Dict[str, Macro] MacroNamespace = Dict[str, ParsedMacro]
# This class builds the MacroResolver by adding macros # This class builds the MacroResolver by adding macros
@@ -21,7 +21,7 @@ MacroNamespace = Dict[str, Macro]
class MacroResolver: class MacroResolver:
def __init__( def __init__(
self, self,
macros: MutableMapping[str, Macro], macros: MutableMapping[str, ParsedMacro],
root_project_name: str, root_project_name: str,
internal_package_names, internal_package_names,
) -> None: ) -> None:
@@ -77,7 +77,7 @@ class MacroResolver:
def _add_macro_to( def _add_macro_to(
self, self,
package_namespaces: Dict[str, MacroNamespace], package_namespaces: Dict[str, MacroNamespace],
macro: Macro, macro: ParsedMacro,
): ):
if macro.package_name in package_namespaces: if macro.package_name in package_namespaces:
namespace = package_namespaces[macro.package_name] namespace = package_namespaces[macro.package_name]
@@ -86,10 +86,10 @@ class MacroResolver:
package_namespaces[macro.package_name] = namespace package_namespaces[macro.package_name] = namespace
if macro.name in namespace: if macro.name in namespace:
raise DuplicateMacroNameError(macro, macro, macro.package_name) raise_duplicate_macro_name(macro, macro, macro.package_name)
package_namespaces[macro.package_name][macro.name] = macro package_namespaces[macro.package_name][macro.name] = macro
def add_macro(self, macro: Macro): def add_macro(self, macro: ParsedMacro):
macro_name: str = macro.name macro_name: str = macro.name
# internal macros (from plugins) will be processed separately from # internal macros (from plugins) will be processed separately from
@@ -109,15 +109,9 @@ class MacroResolver:
def get_macro(self, local_package, macro_name): def get_macro(self, local_package, macro_name):
local_package_macros = {} local_package_macros = {}
# If the macro is explicitly prefixed with an internal namespace
# (e.g. 'dbt.some_macro'), look there first
if local_package in self.internal_package_names:
local_package_macros = self.internal_packages[local_package]
# If the macro is explicitly prefixed with a different package name
# (e.g. 'dbt_utils.some_macro'), look there first
if local_package not in self.internal_package_names and local_package in self.packages: if local_package not in self.internal_package_names and local_package in self.packages:
local_package_macros = self.packages[local_package] local_package_macros = self.packages[local_package]
# First: search the specified package for this macro # First: search the local packages for this macro
if macro_name in local_package_macros: if macro_name in local_package_macros:
return local_package_macros[macro_name] return local_package_macros[macro_name]
# Now look up in the standard search order # Now look up in the standard search order
@@ -187,7 +181,7 @@ class TestMacroNamespace:
elif package_name in self.macro_resolver.packages: elif package_name in self.macro_resolver.packages:
macro = self.macro_resolver.packages[package_name].get(name) macro = self.macro_resolver.packages[package_name].get(name)
else: else:
raise PackageNotFoundForMacroError(package_name) raise_compiler_error(f"Could not find package '{package_name}'")
if not macro: if not macro:
return None return None
macro_func = MacroGenerator(macro, self.ctx, self.node, self.thread_ctx) macro_func = MacroGenerator(macro, self.ctx, self.node, self.thread_ctx)

View File

@@ -1,9 +1,9 @@
from typing import Any, Dict, Iterable, Union, Optional, List, Iterator, Mapping, Set from typing import Any, Dict, Iterable, Union, Optional, List, Iterator, Mapping, Set
from dbt.clients.jinja import MacroGenerator, MacroStack from dbt.clients.jinja import MacroGenerator, MacroStack
from dbt.contracts.graph.nodes import Macro from dbt.contracts.graph.parsed import ParsedMacro
from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
from dbt.exceptions import DuplicateMacroNameError, PackageNotFoundForMacroError from dbt.exceptions import raise_duplicate_macro_name, raise_compiler_error
FlatNamespace = Dict[str, MacroGenerator] FlatNamespace = Dict[str, MacroGenerator]
@@ -75,7 +75,7 @@ class MacroNamespace(Mapping):
elif package_name in self.packages: elif package_name in self.packages:
return self.packages[package_name].get(name) return self.packages[package_name].get(name)
else: else:
raise PackageNotFoundForMacroError(package_name) raise_compiler_error(f"Could not find package '{package_name}'")
# This class builds the MacroNamespace by adding macros to # This class builds the MacroNamespace by adding macros to
@@ -112,7 +112,7 @@ class MacroNamespaceBuilder:
def _add_macro_to( def _add_macro_to(
self, self,
hierarchy: Dict[str, FlatNamespace], hierarchy: Dict[str, FlatNamespace],
macro: Macro, macro: ParsedMacro,
macro_func: MacroGenerator, macro_func: MacroGenerator,
): ):
if macro.package_name in hierarchy: if macro.package_name in hierarchy:
@@ -122,10 +122,10 @@ class MacroNamespaceBuilder:
hierarchy[macro.package_name] = namespace hierarchy[macro.package_name] = namespace
if macro.name in namespace: if macro.name in namespace:
raise DuplicateMacroNameError(macro_func.macro, macro, macro.package_name) raise_duplicate_macro_name(macro_func.macro, macro, macro.package_name)
hierarchy[macro.package_name][macro.name] = macro_func hierarchy[macro.package_name][macro.name] = macro_func
def add_macro(self, macro: Macro, ctx: Dict[str, Any]): def add_macro(self, macro: ParsedMacro, ctx: Dict[str, Any]):
macro_name: str = macro.name macro_name: str = macro.name
# MacroGenerator is in clients/jinja.py # MacroGenerator is in clients/jinja.py
@@ -147,11 +147,13 @@ class MacroNamespaceBuilder:
elif macro.package_name == self.root_package: elif macro.package_name == self.root_package:
self.globals[macro_name] = macro_func self.globals[macro_name] = macro_func
def add_macros(self, macros: Iterable[Macro], ctx: Dict[str, Any]): def add_macros(self, macros: Iterable[ParsedMacro], ctx: Dict[str, Any]):
for macro in macros: for macro in macros:
self.add_macro(macro, ctx) self.add_macro(macro, ctx)
def build_namespace(self, macros: Iterable[Macro], ctx: Dict[str, Any]) -> MacroNamespace: def build_namespace(
self, macros: Iterable[ParsedMacro], ctx: Dict[str, Any]
) -> MacroNamespace:
self.add_macros(macros, ctx) self.add_macros(macros, ctx)
# Iterate in reverse-order and overwrite: the packages that are first # Iterate in reverse-order and overwrite: the packages that are first

View File

@@ -4,7 +4,6 @@ from dbt.clients.jinja import MacroStack
from dbt.contracts.connection import AdapterRequiredConfig from dbt.contracts.connection import AdapterRequiredConfig
from dbt.contracts.graph.manifest import Manifest from dbt.contracts.graph.manifest import Manifest
from dbt.context.macro_resolver import TestMacroNamespace from dbt.context.macro_resolver import TestMacroNamespace
from .base import contextproperty
from .configured import ConfiguredContext from .configured import ConfiguredContext
@@ -67,10 +66,6 @@ class ManifestContext(ConfiguredContext):
dct.update(self.namespace) dct.update(self.namespace)
return dct return dct
@contextproperty
def context_macro_stack(self):
return self.macro_stack
class QueryHeaderContext(ManifestContext): class QueryHeaderContext(ManifestContext):
def __init__(self, config: AdapterRequiredConfig, manifest: Manifest) -> None: def __init__(self, config: AdapterRequiredConfig, manifest: Manifest) -> None:

View File

@@ -19,56 +19,48 @@ from dbt.adapters.factory import get_adapter, get_adapter_package_names, get_ada
from dbt.clients import agate_helper from dbt.clients import agate_helper
from dbt.clients.jinja import get_rendered, MacroGenerator, MacroStack from dbt.clients.jinja import get_rendered, MacroGenerator, MacroStack
from dbt.config import RuntimeConfig, Project from dbt.config import RuntimeConfig, Project
from dbt.constants import SECRET_ENV_PREFIX, DEFAULT_ENV_PLACEHOLDER from .base import contextmember, contextproperty, Var
from dbt.context.base import contextmember, contextproperty, Var from .configured import FQNLookup
from dbt.context.configured import FQNLookup from .context_config import ContextConfig
from dbt.context.context_config import ContextConfig from dbt.logger import SECRET_ENV_PREFIX
from dbt.context.exceptions_jinja import wrapped_exports
from dbt.context.macro_resolver import MacroResolver, TestMacroNamespace from dbt.context.macro_resolver import MacroResolver, TestMacroNamespace
from dbt.context.macros import MacroNamespaceBuilder, MacroNamespace from .macros import MacroNamespaceBuilder, MacroNamespace
from dbt.context.manifest import ManifestContext from .manifest import ManifestContext
from dbt.contracts.connection import AdapterResponse from dbt.contracts.connection import AdapterResponse
from dbt.contracts.graph.manifest import Manifest, Disabled from dbt.contracts.graph.manifest import Manifest, Disabled
from dbt.contracts.graph.nodes import ( from dbt.contracts.graph.compiled import (
Macro, CompiledResource,
Exposure, CompiledSeedNode,
Metric,
Entity,
SeedNode,
SourceDefinition,
Resource,
ManifestNode, ManifestNode,
) )
from dbt.contracts.graph.metrics import MetricReference, ResolvedMetricReference from dbt.contracts.graph.parsed import (
from dbt.events.functions import get_metadata_vars ParsedMacro,
ParsedExposure,
ParsedMetric,
ParsedSeedNode,
ParsedSourceDefinition,
)
from dbt.exceptions import ( from dbt.exceptions import (
CompilationError, CompilationException,
ConflictingConfigKeysError, ParsingException,
SecretEnvVarLocationError, InternalException,
EnvVarMissingError, ValidationException,
DbtInternalError, RuntimeException,
InlineModelConfigError, macro_invalid_dispatch_arg,
NumberSourceArgsError, missing_config,
PersistDocsValueTypeError, raise_compiler_error,
LoadAgateTableNotSeedError, ref_invalid_args,
LoadAgateTableValueError, ref_target_not_found,
MacroDispatchArgError, ref_bad_context,
MacrosSourcesUnWriteableError, source_target_not_found,
MetricArgsError, wrapped_exports,
MissingConfigError, raise_parsing_error,
OperationsCannotRefEphemeralNodesError, disallow_secret_env_var,
PackageNotInDepsError,
ParsingError,
RefBadContextError,
RefArgsError,
DbtRuntimeError,
TargetNotFoundError,
DbtValidationError,
) )
from dbt.config import IsFQNResource from dbt.config import IsFQNResource
from dbt.node_types import NodeType, ModelLanguage from dbt.node_types import NodeType
from dbt.utils import merge, AttrDict, MultiDict, args_to_dict from dbt.utils import merge, AttrDict, MultiDict
from dbt import selected_resources from dbt import selected_resources
@@ -145,10 +137,10 @@ class BaseDatabaseWrapper:
f'`adapter.dispatch("{suggest_macro_name}", ' f'`adapter.dispatch("{suggest_macro_name}", '
f'macro_namespace="{suggest_macro_namespace}")`?' f'macro_namespace="{suggest_macro_namespace}")`?'
) )
raise CompilationError(msg) raise CompilationException(msg)
if packages is not None: if packages is not None:
raise MacroDispatchArgError(macro_name) raise macro_invalid_dispatch_arg(macro_name)
namespace = macro_namespace namespace = macro_namespace
@@ -160,7 +152,7 @@ class BaseDatabaseWrapper:
search_packages = [self.config.project_name, namespace] search_packages = [self.config.project_name, namespace]
else: else:
# Not a string and not None so must be a list # Not a string and not None so must be a list
raise CompilationError( raise CompilationException(
f"In adapter.dispatch, got a list macro_namespace argument " f"In adapter.dispatch, got a list macro_namespace argument "
f'("{macro_namespace}"), but macro_namespace should be None or a string.' f'("{macro_namespace}"), but macro_namespace should be None or a string.'
) )
@@ -173,8 +165,8 @@ class BaseDatabaseWrapper:
try: try:
# this uses the namespace from the context # this uses the namespace from the context
macro = self._namespace.get_from_package(package_name, search_name) macro = self._namespace.get_from_package(package_name, search_name)
except CompilationError: except CompilationException:
# Only raise CompilationError if macro is not found in # Only raise CompilationException if macro is not found in
# any package # any package
macro = None macro = None
@@ -187,8 +179,8 @@ class BaseDatabaseWrapper:
return macro return macro
searched = ", ".join(repr(a) for a in attempts) searched = ", ".join(repr(a) for a in attempts)
msg = f"In dispatch: No macro named '{macro_name}' found\n Searched for: {searched}" msg = f"In dispatch: No macro named '{macro_name}' found\n" f" Searched for: {searched}"
raise CompilationError(msg) raise CompilationException(msg)
class BaseResolver(metaclass=abc.ABCMeta): class BaseResolver(metaclass=abc.ABCMeta):
@@ -207,7 +199,7 @@ class BaseResolver(metaclass=abc.ABCMeta):
return self.db_wrapper.Relation return self.db_wrapper.Relation
@abc.abstractmethod @abc.abstractmethod
def __call__(self, *args: str) -> Union[str, RelationProxy, MetricReference]: def __call__(self, *args: str) -> Union[str, RelationProxy]:
pass pass
@@ -224,13 +216,13 @@ class BaseRefResolver(BaseResolver):
def validate_args(self, name: str, package: Optional[str]): def validate_args(self, name: str, package: Optional[str]):
if not isinstance(name, str): if not isinstance(name, str):
raise CompilationError( raise CompilationException(
f"The name argument to ref() must be a string, got {type(name)}" f"The name argument to ref() must be a string, got " f"{type(name)}"
) )
if package is not None and not isinstance(package, str): if package is not None and not isinstance(package, str):
raise CompilationError( raise CompilationException(
f"The package argument to ref() must be a string or None, got {type(package)}" f"The package argument to ref() must be a string or None, got " f"{type(package)}"
) )
def __call__(self, *args: str) -> RelationProxy: def __call__(self, *args: str) -> RelationProxy:
@@ -242,7 +234,7 @@ class BaseRefResolver(BaseResolver):
elif len(args) == 2: elif len(args) == 2:
package, name = args package, name = args
else: else:
raise RefArgsError(node=self.model, args=args) ref_invalid_args(self.model, args)
self.validate_args(name, package) self.validate_args(name, package)
return self.resolve(name, package) return self.resolve(name, package)
@@ -254,58 +246,25 @@ class BaseSourceResolver(BaseResolver):
def validate_args(self, source_name: str, table_name: str): def validate_args(self, source_name: str, table_name: str):
if not isinstance(source_name, str): if not isinstance(source_name, str):
raise CompilationError( raise CompilationException(
f"The source name (first) argument to source() must be a " f"The source name (first) argument to source() must be a "
f"string, got {type(source_name)}" f"string, got {type(source_name)}"
) )
if not isinstance(table_name, str): if not isinstance(table_name, str):
raise CompilationError( raise CompilationException(
f"The table name (second) argument to source() must be a " f"The table name (second) argument to source() must be a "
f"string, got {type(table_name)}" f"string, got {type(table_name)}"
) )
def __call__(self, *args: str) -> RelationProxy: def __call__(self, *args: str) -> RelationProxy:
if len(args) != 2: if len(args) != 2:
raise NumberSourceArgsError(args, node=self.model) raise_compiler_error(
f"source() takes exactly two arguments ({len(args)} given)", self.model
)
self.validate_args(args[0], args[1]) self.validate_args(args[0], args[1])
return self.resolve(args[0], args[1]) return self.resolve(args[0], args[1])
class BaseMetricResolver(BaseResolver):
def resolve(self, name: str, package: Optional[str] = None) -> MetricReference:
...
def _repack_args(self, name: str, package: Optional[str]) -> List[str]:
if package is None:
return [name]
else:
return [package, name]
def validate_args(self, name: str, package: Optional[str]):
if not isinstance(name, str):
raise CompilationError(
f"The name argument to metric() must be a string, got {type(name)}"
)
if package is not None and not isinstance(package, str):
raise CompilationError(
f"The package argument to metric() must be a string or None, got {type(package)}"
)
def __call__(self, *args: str) -> MetricReference:
name: str
package: Optional[str] = None
if len(args) == 1:
name = args[0]
elif len(args) == 2:
package, name = args
else:
raise MetricArgsError(node=self.model, args=args)
self.validate_args(name, package)
return self.resolve(name, package)
class Config(Protocol): class Config(Protocol):
def __init__(self, model, context_config: Optional[ContextConfig]): def __init__(self, model, context_config: Optional[ContextConfig]):
... ...
@@ -322,7 +281,12 @@ class ParseConfigObject(Config):
if oldkey in config: if oldkey in config:
newkey = oldkey.replace("_", "-") newkey = oldkey.replace("_", "-")
if newkey in config: if newkey in config:
raise ConflictingConfigKeysError(oldkey, newkey, node=self.model) raise_compiler_error(
'Invalid config, has conflicting keys "{}" and "{}"'.format(
oldkey, newkey
),
self.model,
)
config[newkey] = config.pop(oldkey) config[newkey] = config.pop(oldkey)
return config return config
@@ -332,14 +296,14 @@ class ParseConfigObject(Config):
elif len(args) == 0 and len(kwargs) > 0: elif len(args) == 0 and len(kwargs) > 0:
opts = kwargs opts = kwargs
else: else:
raise InlineModelConfigError(node=self.model) raise_compiler_error("Invalid inline model config", self.model)
opts = self._transform_config(opts) opts = self._transform_config(opts)
# it's ok to have a parse context with no context config, but you must # it's ok to have a parse context with no context config, but you must
# not call it! # not call it!
if self.context_config is None: if self.context_config is None:
raise DbtRuntimeError("At parse time, did not receive a context config") raise RuntimeException("At parse time, did not receive a context config")
self.context_config.add_config_call(opts) self.context_config.add_config_call(opts)
return "" return ""
@@ -380,7 +344,7 @@ class RuntimeConfigObject(Config):
else: else:
result = self.model.config.get(name, default) result = self.model.config.get(name, default)
if result is _MISSING: if result is _MISSING:
raise MissingConfigError(unique_id=self.model.unique_id, name=name) missing_config(self.model, name)
return result return result
def require(self, name, validator=None): def require(self, name, validator=None):
@@ -402,14 +366,20 @@ class RuntimeConfigObject(Config):
def persist_relation_docs(self) -> bool: def persist_relation_docs(self) -> bool:
persist_docs = self.get("persist_docs", default={}) persist_docs = self.get("persist_docs", default={})
if not isinstance(persist_docs, dict): if not isinstance(persist_docs, dict):
raise PersistDocsValueTypeError(persist_docs) raise_compiler_error(
f"Invalid value provided for 'persist_docs'. Expected dict "
f"but received {type(persist_docs)}"
)
return persist_docs.get("relation", False) return persist_docs.get("relation", False)
def persist_column_docs(self) -> bool: def persist_column_docs(self) -> bool:
persist_docs = self.get("persist_docs", default={}) persist_docs = self.get("persist_docs", default={})
if not isinstance(persist_docs, dict): if not isinstance(persist_docs, dict):
raise PersistDocsValueTypeError(persist_docs) raise_compiler_error(
f"Invalid value provided for 'persist_docs'. Expected dict "
f"but received {type(persist_docs)}"
)
return persist_docs.get("columns", False) return persist_docs.get("columns", False)
@@ -468,11 +438,10 @@ class RuntimeRefResolver(BaseRefResolver):
) )
if target_model is None or isinstance(target_model, Disabled): if target_model is None or isinstance(target_model, Disabled):
raise TargetNotFoundError( ref_target_not_found(
node=self.model, self.model,
target_name=target_name, target_name,
target_kind="node", target_package,
target_package=target_package,
disabled=isinstance(target_model, Disabled), disabled=isinstance(target_model, Disabled),
) )
self.validate(target_model, target_name, target_package) self.validate(target_model, target_name, target_package)
@@ -490,7 +459,7 @@ class RuntimeRefResolver(BaseRefResolver):
) -> None: ) -> None:
if resolved.unique_id not in self.model.depends_on.nodes: if resolved.unique_id not in self.model.depends_on.nodes:
args = self._repack_args(target_name, target_package) args = self._repack_args(target_name, target_package)
raise RefBadContextError(node=self.model, args=args) ref_bad_context(self.model, args)
class OperationRefResolver(RuntimeRefResolver): class OperationRefResolver(RuntimeRefResolver):
@@ -505,8 +474,13 @@ class OperationRefResolver(RuntimeRefResolver):
def create_relation(self, target_model: ManifestNode, name: str) -> RelationProxy: def create_relation(self, target_model: ManifestNode, name: str) -> RelationProxy:
if target_model.is_ephemeral_model: if target_model.is_ephemeral_model:
# In operations, we can't ref() ephemeral nodes, because # In operations, we can't ref() ephemeral nodes, because
# Macros do not support set_cte # ParsedMacros do not support set_cte
raise OperationsCannotRefEphemeralNodesError(target_model.name, node=self.model) raise_compiler_error(
"Operations can not ref() ephemeral nodes, but {} is ephemeral".format(
target_model.name
),
self.model,
)
else: else:
return super().create_relation(target_model, name) return super().create_relation(target_model, name)
@@ -529,52 +503,23 @@ class RuntimeSourceResolver(BaseSourceResolver):
) )
if target_source is None or isinstance(target_source, Disabled): if target_source is None or isinstance(target_source, Disabled):
raise TargetNotFoundError( source_target_not_found(
node=self.model, self.model,
target_name=f"{source_name}.{table_name}", source_name,
target_kind="source", table_name,
disabled=(isinstance(target_source, Disabled)),
) )
return self.Relation.create_from_source(target_source) return self.Relation.create_from_source(target_source)
# metric` implementations
class ParseMetricResolver(BaseMetricResolver):
def resolve(self, name: str, package: Optional[str] = None) -> MetricReference:
self.model.metrics.append(self._repack_args(name, package))
return MetricReference(name, package)
class RuntimeMetricResolver(BaseMetricResolver):
def resolve(self, target_name: str, target_package: Optional[str] = None) -> MetricReference:
target_metric = self.manifest.resolve_metric(
target_name,
target_package,
self.current_project,
self.model.package_name,
)
if target_metric is None or isinstance(target_metric, Disabled):
raise TargetNotFoundError(
node=self.model,
target_name=target_name,
target_kind="metric",
target_package=target_package,
)
return ResolvedMetricReference(target_metric, self.manifest, self.Relation)
# `var` implementations. # `var` implementations.
class ModelConfiguredVar(Var): class ModelConfiguredVar(Var):
def __init__( def __init__(
self, self,
context: Dict[str, Any], context: Dict[str, Any],
config: RuntimeConfig, config: RuntimeConfig,
node: Resource, node: CompiledResource,
) -> None: ) -> None:
self._node: Resource self._node: CompiledResource
self._config: RuntimeConfig = config self._config: RuntimeConfig = config
super().__init__(context, config.cli_vars, node=node) super().__init__(context, config.cli_vars, node=node)
@@ -585,7 +530,7 @@ class ModelConfiguredVar(Var):
if package_name != self._config.project_name: if package_name != self._config.project_name:
if package_name not in dependencies: if package_name not in dependencies:
# I don't think this is actually reachable # I don't think this is actually reachable
raise PackageNotInDepsError(package_name, node=self._node) raise_compiler_error(f"Node package named {package_name} not found!", self._node)
yield dependencies[package_name] yield dependencies[package_name]
yield self._config yield self._config
@@ -623,7 +568,6 @@ class Provider(Protocol):
Var: Type[ModelConfiguredVar] Var: Type[ModelConfiguredVar]
ref: Type[BaseRefResolver] ref: Type[BaseRefResolver]
source: Type[BaseSourceResolver] source: Type[BaseSourceResolver]
metric: Type[BaseMetricResolver]
class ParseProvider(Provider): class ParseProvider(Provider):
@@ -633,7 +577,6 @@ class ParseProvider(Provider):
Var = ParseVar Var = ParseVar
ref = ParseRefResolver ref = ParseRefResolver
source = ParseSourceResolver source = ParseSourceResolver
metric = ParseMetricResolver
class GenerateNameProvider(Provider): class GenerateNameProvider(Provider):
@@ -643,7 +586,6 @@ class GenerateNameProvider(Provider):
Var = RuntimeVar Var = RuntimeVar
ref = ParseRefResolver ref = ParseRefResolver
source = ParseSourceResolver source = ParseSourceResolver
metric = ParseMetricResolver
class RuntimeProvider(Provider): class RuntimeProvider(Provider):
@@ -653,7 +595,6 @@ class RuntimeProvider(Provider):
Var = RuntimeVar Var = RuntimeVar
ref = RuntimeRefResolver ref = RuntimeRefResolver
source = RuntimeSourceResolver source = RuntimeSourceResolver
metric = RuntimeMetricResolver
class OperationProvider(RuntimeProvider): class OperationProvider(RuntimeProvider):
@@ -675,10 +616,10 @@ class ProviderContext(ManifestContext):
context_config: Optional[ContextConfig], context_config: Optional[ContextConfig],
) -> None: ) -> None:
if provider is None: if provider is None:
raise DbtInternalError(f"Invalid provider given to context: {provider}") raise InternalException(f"Invalid provider given to context: {provider}")
# mypy appeasement - we know it'll be a RuntimeConfig # mypy appeasement - we know it'll be a RuntimeConfig
self.config: RuntimeConfig self.config: RuntimeConfig
self.model: Union[Macro, ManifestNode] = model self.model: Union[ParsedMacro, ManifestNode] = model
super().__init__(config, manifest, model.package_name) super().__init__(config, manifest, model.package_name)
self.sql_results: Dict[str, AttrDict] = {} self.sql_results: Dict[str, AttrDict] = {}
self.context_config: Optional[ContextConfig] = context_config self.context_config: Optional[ContextConfig] = context_config
@@ -699,14 +640,6 @@ class ProviderContext(ManifestContext):
self.model, self.model,
) )
@contextproperty
def dbt_metadata_envs(self) -> Dict[str, str]:
return get_metadata_vars()
@contextproperty
def invocation_args_dict(self):
return args_to_dict(self.config.args)
@contextproperty @contextproperty
def _sql_results(self) -> Dict[str, AttrDict]: def _sql_results(self) -> Dict[str, AttrDict]:
return self.sql_results return self.sql_results
@@ -752,7 +685,7 @@ class ProviderContext(ManifestContext):
return return
elif value == arg: elif value == arg:
return return
raise DbtValidationError( raise ValidationException(
'Expected value "{}" to be one of {}'.format(value, ",".join(map(str, args))) 'Expected value "{}" to be one of {}'.format(value, ",".join(map(str, args)))
) )
@@ -767,8 +700,8 @@ class ProviderContext(ManifestContext):
@contextmember @contextmember
def write(self, payload: str) -> str: def write(self, payload: str) -> str:
# macros/source defs aren't 'writeable'. # macros/source defs aren't 'writeable'.
if isinstance(self.model, (Macro, SourceDefinition)): if isinstance(self.model, (ParsedMacro, ParsedSourceDefinition)):
raise MacrosSourcesUnWriteableError(node=self.model) raise_compiler_error('cannot "write" macros or sources')
self.model.build_path = self.model.write_node(self.config.target_path, "run", payload) self.model.build_path = self.model.write_node(self.config.target_path, "run", payload)
return "" return ""
@@ -783,19 +716,20 @@ class ProviderContext(ManifestContext):
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
except Exception: except Exception:
raise CompilationError(message_if_exception, self.model) raise_compiler_error(message_if_exception, self.model)
@contextmember @contextmember
def load_agate_table(self) -> agate.Table: def load_agate_table(self) -> agate.Table:
if not isinstance(self.model, SeedNode): if not isinstance(self.model, (ParsedSeedNode, CompiledSeedNode)):
raise LoadAgateTableNotSeedError(self.model.resource_type, node=self.model) raise_compiler_error(
assert self.model.root_path "can only load_agate_table for seeds (got a {})".format(self.model.resource_type)
)
path = os.path.join(self.model.root_path, self.model.original_file_path) path = os.path.join(self.model.root_path, self.model.original_file_path)
column_types = self.model.config.column_types column_types = self.model.config.column_types
try: try:
table = agate_helper.from_csv(path, text_columns=column_types) table = agate_helper.from_csv(path, text_columns=column_types)
except ValueError as e: except ValueError as e:
raise LoadAgateTableValueError(e, node=self.model) raise_compiler_error(str(e))
table.original_abspath = os.path.abspath(path) table.original_abspath = os.path.abspath(path)
return table return table
@@ -844,10 +778,6 @@ class ProviderContext(ManifestContext):
def source(self) -> Callable: def source(self) -> Callable:
return self.provider.source(self.db_wrapper, self.model, self.config, self.manifest) return self.provider.source(self.db_wrapper, self.model, self.config, self.manifest)
@contextproperty
def metric(self) -> Callable:
return self.provider.metric(self.db_wrapper, self.model, self.config, self.manifest)
@contextproperty("config") @contextproperty("config")
def ctx_config(self) -> Config: def ctx_config(self) -> Config:
"""The `config` variable exists to handle end-user configuration for """The `config` variable exists to handle end-user configuration for
@@ -1153,12 +1083,7 @@ class ProviderContext(ManifestContext):
@contextproperty("model") @contextproperty("model")
def ctx_model(self) -> Dict[str, Any]: def ctx_model(self) -> Dict[str, Any]:
ret = self.model.to_dict(omit_none=True) return self.model.to_dict(omit_none=True)
# Maintain direct use of compiled_sql
# TODO add depreciation logic[CT-934]
if "compiled_code" in ret:
ret["compiled_sql"] = ret["compiled_code"]
return ret
@contextproperty @contextproperty
def pre_hooks(self) -> Optional[List[Dict[str, Any]]]: def pre_hooks(self) -> Optional[List[Dict[str, Any]]]:
@@ -1186,7 +1111,7 @@ class ProviderContext(ManifestContext):
"https://docs.getdbt.com/reference/dbt-jinja-functions/dispatch)" "https://docs.getdbt.com/reference/dbt-jinja-functions/dispatch)"
" adapter_macro was called for: {macro_name}".format(macro_name=name) " adapter_macro was called for: {macro_name}".format(macro_name=name)
) )
raise CompilationError(msg) raise CompilationException(msg)
@contextmember @contextmember
def env_var(self, var: str, default: Optional[str] = None) -> str: def env_var(self, var: str, default: Optional[str] = None) -> str:
@@ -1197,7 +1122,7 @@ class ProviderContext(ManifestContext):
""" """
return_value = None return_value = None
if var.startswith(SECRET_ENV_PREFIX): if var.startswith(SECRET_ENV_PREFIX):
raise SecretEnvVarLocationError(var) disallow_secret_env_var(var)
if var in os.environ: if var in os.environ:
return_value = os.environ[var] return_value = os.environ[var]
elif default is not None: elif default is not None:
@@ -1206,21 +1131,8 @@ class ProviderContext(ManifestContext):
if return_value is not None: if return_value is not None:
# Save the env_var value in the manifest and the var name in the source_file. # Save the env_var value in the manifest and the var name in the source_file.
# If this is compiling, do not save because it's irrelevant to parsing. # If this is compiling, do not save because it's irrelevant to parsing.
compiling = ( if self.model and not hasattr(self.model, "compiled"):
True self.manifest.env_vars[var] = return_value
if hasattr(self.model, "compiled")
and getattr(self.model, "compiled", False) is True
else False
)
if self.model and not compiling:
# If the environment variable is set from a default, store a string indicating
# that so we can skip partial parsing. Otherwise the file will be scheduled for
# reparsing. If the default changes, the file will have been updated and therefore
# will be scheduled for reparsing anyways.
self.manifest.env_vars[var] = (
return_value if var in os.environ else DEFAULT_ENV_PLACEHOLDER
)
# hooks come from dbt_project.yml which doesn't have a real file_id # hooks come from dbt_project.yml which doesn't have a real file_id
if self.model.file_id in self.manifest.files: if self.model.file_id in self.manifest.files:
source_file = self.manifest.files[self.model.file_id] source_file = self.manifest.files[self.model.file_id]
@@ -1230,7 +1142,8 @@ class ProviderContext(ManifestContext):
source_file.env_vars.append(var) # type: ignore[union-attr] source_file.env_vars.append(var) # type: ignore[union-attr]
return return_value return return_value
else: else:
raise EnvVarMissingError(var) msg = f"Env var required but not provided: '{var}'"
raise_parsing_error(msg)
@contextproperty @contextproperty
def selected_resources(self) -> List[str]: def selected_resources(self) -> List[str]:
@@ -1241,19 +1154,6 @@ class ProviderContext(ManifestContext):
""" """
return selected_resources.SELECTED_RESOURCES return selected_resources.SELECTED_RESOURCES
@contextmember
def submit_python_job(self, parsed_model: Dict, compiled_code: str) -> AdapterResponse:
# Check macro_stack and that the unique id is for a materialization macro
if not (
self.context_macro_stack.depth == 2
and self.context_macro_stack.call_stack[1] == "macro.dbt.statement"
and "materialization" in self.context_macro_stack.call_stack[0]
):
raise DbtRuntimeError(
f"submit_python_job is not intended to be called here, at model {parsed_model['alias']}, with macro call_stack {self.context_macro_stack.call_stack}."
)
return self.adapter.submit_python_job(parsed_model, compiled_code)
class MacroContext(ProviderContext): class MacroContext(ProviderContext):
"""Internally, macros can be executed like nodes, with some restrictions: """Internally, macros can be executed like nodes, with some restrictions:
@@ -1266,7 +1166,7 @@ class MacroContext(ProviderContext):
def __init__( def __init__(
self, self,
model: Macro, model: ParsedMacro,
config: RuntimeConfig, config: RuntimeConfig,
manifest: Manifest, manifest: Manifest,
provider: Provider, provider: Provider,
@@ -1304,20 +1204,9 @@ class ModelContext(ProviderContext):
@contextproperty @contextproperty
def sql(self) -> Optional[str]: def sql(self) -> Optional[str]:
# only doing this in sql model for backward compatible
if (
getattr(self.model, "extra_ctes_injected", None)
and self.model.language == ModelLanguage.sql # type: ignore[union-attr]
):
# TODO CT-211
return self.model.compiled_code # type: ignore[union-attr]
return None
@contextproperty
def compiled_code(self) -> Optional[str]:
if getattr(self.model, "extra_ctes_injected", None): if getattr(self.model, "extra_ctes_injected", None):
# TODO CT-211 # TODO CT-211
return self.model.compiled_code # type: ignore[union-attr] return self.model.compiled_sql # type: ignore[union-attr]
return None return None
@contextproperty @contextproperty
@@ -1381,7 +1270,7 @@ def generate_parser_model_context(
def generate_generate_name_macro_context( def generate_generate_name_macro_context(
macro: Macro, macro: ParsedMacro,
config: RuntimeConfig, config: RuntimeConfig,
manifest: Manifest, manifest: Manifest,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@@ -1399,7 +1288,7 @@ def generate_runtime_model_context(
def generate_runtime_macro_context( def generate_runtime_macro_context(
macro: Macro, macro: ParsedMacro,
config: RuntimeConfig, config: RuntimeConfig,
manifest: Manifest, manifest: Manifest,
package_name: Optional[str], package_name: Optional[str],
@@ -1411,7 +1300,7 @@ def generate_runtime_macro_context(
class ExposureRefResolver(BaseResolver): class ExposureRefResolver(BaseResolver):
def __call__(self, *args) -> str: def __call__(self, *args) -> str:
if len(args) not in (1, 2): if len(args) not in (1, 2):
raise RefArgsError(node=self.model, args=args) ref_invalid_args(self.model, args)
self.model.refs.append(list(args)) self.model.refs.append(list(args))
return "" return ""
@@ -1419,21 +1308,15 @@ class ExposureRefResolver(BaseResolver):
class ExposureSourceResolver(BaseResolver): class ExposureSourceResolver(BaseResolver):
def __call__(self, *args) -> str: def __call__(self, *args) -> str:
if len(args) != 2: if len(args) != 2:
raise NumberSourceArgsError(args, node=self.model) raise_compiler_error(
f"source() takes exactly two arguments ({len(args)} given)", self.model
)
self.model.sources.append(list(args)) self.model.sources.append(list(args))
return "" return ""
class ExposureMetricResolver(BaseResolver):
def __call__(self, *args) -> str:
if len(args) not in (1, 2):
raise MetricArgsError(node=self.model, args=args)
self.model.metrics.append(list(args))
return ""
def generate_parse_exposure( def generate_parse_exposure(
exposure: Exposure, exposure: ParsedExposure,
config: RuntimeConfig, config: RuntimeConfig,
manifest: Manifest, manifest: Manifest,
package_name: str, package_name: str,
@@ -1452,12 +1335,6 @@ def generate_parse_exposure(
project, project,
manifest, manifest,
), ),
"metric": ExposureMetricResolver(
None,
exposure,
project,
manifest,
),
} }
@@ -1469,21 +1346,21 @@ class MetricRefResolver(BaseResolver):
elif len(args) == 2: elif len(args) == 2:
package, name = args package, name = args
else: else:
raise RefArgsError(node=self.model, args=args) ref_invalid_args(self.model, args)
self.validate_args(name, package) self.validate_args(name, package)
self.model.refs.append(list(args)) self.model.refs.append(list(args))
return "" return ""
def validate_args(self, name, package): def validate_args(self, name, package):
if not isinstance(name, str): if not isinstance(name, str):
raise ParsingError( raise ParsingException(
f"In a metrics section in {self.model.original_file_path} " f"In a metrics section in {self.model.original_file_path} "
"the name argument to ref() must be a string" f"the name argument to ref() must be a string"
) )
def generate_parse_metrics( def generate_parse_metrics(
metric: Metric, metric: ParsedMetric,
config: RuntimeConfig, config: RuntimeConfig,
manifest: Manifest, manifest: Manifest,
package_name: str, package_name: str,
@@ -1496,50 +1373,6 @@ def generate_parse_metrics(
project, project,
manifest, manifest,
), ),
"metric": ParseMetricResolver(
None,
metric,
project,
manifest,
),
}
class EntityRefResolver(BaseResolver):
def __call__(self, *args) -> str:
package = None
if len(args) == 1:
name = args[0]
elif len(args) == 2:
package, name = args
else:
raise RefArgsError(node=self.model, args=args)
self.validate_args(name, package)
self.model.refs.append(list(args))
return ""
def validate_args(self, name, package):
if not isinstance(name, str):
raise ParsingError(
f"In the entity associated with {self.model.original_file_path} "
"the name argument to ref() must be a string"
)
def generate_parse_entities(
entity: Entity,
config: RuntimeConfig,
manifest: Manifest,
package_name: str,
) -> Dict[str, Any]:
project = config.load_dependencies()[package_name]
return {
"ref": EntityRefResolver(
None,
entity,
project,
manifest,
),
} }
@@ -1597,7 +1430,7 @@ class TestContext(ProviderContext):
def env_var(self, var: str, default: Optional[str] = None) -> str: def env_var(self, var: str, default: Optional[str] = None) -> str:
return_value = None return_value = None
if var.startswith(SECRET_ENV_PREFIX): if var.startswith(SECRET_ENV_PREFIX):
raise SecretEnvVarLocationError(var) disallow_secret_env_var(var)
if var in os.environ: if var in os.environ:
return_value = os.environ[var] return_value = os.environ[var]
elif default is not None: elif default is not None:
@@ -1606,13 +1439,7 @@ class TestContext(ProviderContext):
if return_value is not None: if return_value is not None:
# Save the env_var value in the manifest and the var name in the source_file # Save the env_var value in the manifest and the var name in the source_file
if self.model: if self.model:
# If the environment variable is set from a default, store a string indicating self.manifest.env_vars[var] = return_value
# that so we can skip partial parsing. Otherwise the file will be scheduled for
# reparsing. If the default changes, the file will have been updated and therefore
# will be scheduled for reparsing anyways.
self.manifest.env_vars[var] = (
return_value if var in os.environ else DEFAULT_ENV_PLACEHOLDER
)
# the "model" should only be test nodes, but just in case, check # the "model" should only be test nodes, but just in case, check
# TODO CT-211 # TODO CT-211
if self.model.resource_type == NodeType.Test and self.model.file_key_name: # type: ignore[union-attr] # noqa if self.model.resource_type == NodeType.Test and self.model.file_key_name: # type: ignore[union-attr] # noqa
@@ -1623,7 +1450,8 @@ class TestContext(ProviderContext):
source_file.add_env_var(var, yaml_key, name) # type: ignore[union-attr] source_file.add_env_var(var, yaml_key, name) # type: ignore[union-attr]
return return_value return return_value
else: else:
raise EnvVarMissingError(var) msg = f"Env var required but not provided: '{var}'"
raise_parsing_error(msg)
def generate_test_context( def generate_test_context(

View File

@@ -3,11 +3,8 @@ from typing import Any, Dict, Optional
from .base import BaseContext, contextmember from .base import BaseContext, contextmember
from dbt.constants import SECRET_ENV_PREFIX, DEFAULT_ENV_PLACEHOLDER from dbt.exceptions import raise_parsing_error
from dbt.exceptions import EnvVarMissingError from dbt.logger import SECRET_ENV_PREFIX
SECRET_PLACEHOLDER = "$$$DBT_SECRET_START$$${}$$$DBT_SECRET_END$$$"
class SecretContext(BaseContext): class SecretContext(BaseContext):
@@ -21,36 +18,25 @@ class SecretContext(BaseContext):
If the default is None, raise an exception for an undefined variable. If the default is None, raise an exception for an undefined variable.
In this context *only*, env_var will accept env vars prefixed with DBT_ENV_SECRET_. In this context *only*, env_var will return the actual values of
It will return the name of the secret env var, wrapped in 'start' and 'end' identifiers. env vars prefixed with DBT_ENV_SECRET_
The actual value will be subbed in later in SecretRenderer.render_value()
""" """
return_value = None return_value = None
if var in os.environ:
# if this is a 'secret' env var, just return the name of the env var
# instead of rendering the actual value here, to avoid any risk of
# Jinja manipulation. it will be subbed out later, in SecretRenderer.render_value
if var in os.environ and var.startswith(SECRET_ENV_PREFIX):
return SECRET_PLACEHOLDER.format(var)
elif var in os.environ:
return_value = os.environ[var] return_value = os.environ[var]
elif default is not None: elif default is not None:
return_value = default return_value = default
if return_value is not None: if return_value is not None:
# store env vars in the internal manifest to power partial parsing # do not save secret environment variables
# if it's a 'secret' env var, we shouldn't even get here
# but just to be safe — don't save secrets
if not var.startswith(SECRET_ENV_PREFIX): if not var.startswith(SECRET_ENV_PREFIX):
# If the environment variable is set from a default, store a string indicating self.env_vars[var] = return_value
# that so we can skip partial parsing. Otherwise the file will be scheduled for
# reparsing. If the default changes, the file will have been updated and therefore # return the value even if its a secret
# will be scheduled for reparsing anyways.
self.env_vars[var] = return_value if var in os.environ else DEFAULT_ENV_PLACEHOLDER
return return_value return return_value
else: else:
raise EnvVarMissingError(var) msg = f"Env var required but not provided: '{var}'"
raise_parsing_error(msg)
def generate_secret_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]: def generate_secret_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]:

View File

@@ -12,11 +12,10 @@ from typing import (
List, List,
Callable, Callable,
) )
from dbt.exceptions import DbtInternalError from dbt.exceptions import InternalException
from dbt.utils import translate_aliases from dbt.utils import translate_aliases
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import NewConnectionOpening from dbt.events.types import NewConnectionOpening
from dbt.events.contextvars import get_node_info
from typing_extensions import Protocol from typing_extensions import Protocol
from dbt.dataclass_schema import ( from dbt.dataclass_schema import (
dbtClassMixin, dbtClassMixin,
@@ -94,8 +93,8 @@ class Connection(ExtensibleDbtClassMixin, Replaceable):
# this will actually change 'self._handle'. # this will actually change 'self._handle'.
self._handle.resolve(self) self._handle.resolve(self)
except RecursionError as exc: except RecursionError as exc:
raise DbtInternalError( raise InternalException(
"A connection's open() method attempted to read the handle value" "A connection's open() method attempted to read the " "handle value"
) from exc ) from exc
return self._handle return self._handle
@@ -113,9 +112,7 @@ class LazyHandle:
self.opener = opener self.opener = opener
def resolve(self, connection: Connection) -> Connection: def resolve(self, connection: Connection) -> Connection:
fire_event( fire_event(NewConnectionOpening(connection_state=connection.state))
NewConnectionOpening(connection_state=connection.state, node_info=get_node_info())
)
return self.opener(connection) return self.opener(connection)

View File

@@ -1,16 +1,18 @@
import hashlib import hashlib
import os import os
from dataclasses import dataclass, field from dataclasses import dataclass, field
from mashumaro.types import SerializableType from mashumaro.types import SerializableType
from typing import List, Optional, Union, Dict, Any from typing import List, Optional, Union, Dict, Any
from dbt.constants import MAXIMUM_SEED_SIZE
from dbt.dataclass_schema import dbtClassMixin, StrEnum from dbt.dataclass_schema import dbtClassMixin, StrEnum
from .util import SourceKey from .util import SourceKey
MAXIMUM_SEED_SIZE = 1 * 1024 * 1024
MAXIMUM_SEED_SIZE_NAME = "1MB"
class ParseFileType(StrEnum): class ParseFileType(StrEnum):
Macro = "macro" Macro = "macro"
Model = "model" Model = "model"
@@ -112,34 +114,25 @@ class FileHash(dbtClassMixin):
@dataclass @dataclass
class RemoteFile(dbtClassMixin): class RemoteFile(dbtClassMixin):
def __init__(self, language) -> None:
if language == "sql":
self.path_end = ".sql"
elif language == "python":
self.path_end = ".py"
else:
raise RuntimeError(f"Invalid language for remote File {language}")
self.path = f"from remote system{self.path_end}"
@property @property
def searched_path(self) -> str: def searched_path(self) -> str:
return self.path return "from remote system"
@property @property
def relative_path(self) -> str: def relative_path(self) -> str:
return self.path return "from remote system"
@property @property
def absolute_path(self) -> str: def absolute_path(self) -> str:
return self.path return "from remote system"
@property @property
def original_file_path(self): def original_file_path(self):
return self.path return "from remote system"
@property @property
def modification_time(self): def modification_time(self):
return self.path return "from remote system"
@dataclass @dataclass
@@ -209,9 +202,9 @@ class SourceFile(BaseSourceFile):
# TODO: do this a different way. This remote file kludge isn't going # TODO: do this a different way. This remote file kludge isn't going
# to work long term # to work long term
@classmethod @classmethod
def remote(cls, contents: str, project_name: str, language: str) -> "SourceFile": def remote(cls, contents: str, project_name: str) -> "SourceFile":
self = cls( self = cls(
path=RemoteFile(language), path=RemoteFile(),
checksum=FileHash.from_contents(contents), checksum=FileHash.from_contents(contents),
project_name=project_name, project_name=project_name,
contents=contents, contents=contents,
@@ -227,7 +220,6 @@ class SchemaSourceFile(BaseSourceFile):
sources: List[str] = field(default_factory=list) sources: List[str] = field(default_factory=list)
exposures: List[str] = field(default_factory=list) exposures: List[str] = field(default_factory=list)
metrics: List[str] = field(default_factory=list) metrics: List[str] = field(default_factory=list)
entities: List[str] = field(default_factory=list)
# node patches contain models, seeds, snapshots, analyses # node patches contain models, seeds, snapshots, analyses
ndp: List[str] = field(default_factory=list) ndp: List[str] = field(default_factory=list)
# any macro patches in this file by macro unique_id. # any macro patches in this file by macro unique_id.
@@ -276,13 +268,11 @@ class SchemaSourceFile(BaseSourceFile):
self.tests[key][name] = [] self.tests[key][name] = []
self.tests[key][name].append(node_unique_id) self.tests[key][name].append(node_unique_id)
# this is only used in unit tests
def remove_tests(self, yaml_key, name): def remove_tests(self, yaml_key, name):
if yaml_key in self.tests: if yaml_key in self.tests:
if name in self.tests[yaml_key]: if name in self.tests[yaml_key]:
del self.tests[yaml_key][name] del self.tests[yaml_key][name]
# this is only used in tests (unit + functional)
def get_tests(self, yaml_key, name): def get_tests(self, yaml_key, name):
if yaml_key in self.tests: if yaml_key in self.tests:
if name in self.tests[yaml_key]: if name in self.tests[yaml_key]:

View File

@@ -0,0 +1,235 @@
from dbt.contracts.graph.parsed import (
HasTestMetadata,
ParsedNode,
ParsedAnalysisNode,
ParsedSingularTestNode,
ParsedHookNode,
ParsedModelNode,
ParsedExposure,
ParsedMetric,
ParsedResource,
ParsedRPCNode,
ParsedSqlNode,
ParsedGenericTestNode,
ParsedSeedNode,
ParsedSnapshotNode,
ParsedSourceDefinition,
SeedConfig,
TestConfig,
same_seeds,
)
from dbt.node_types import NodeType
from dbt.contracts.util import Replaceable
from dbt.dataclass_schema import dbtClassMixin
from dataclasses import dataclass, field
from typing import Optional, List, Union, Dict, Type
@dataclass
class InjectedCTE(dbtClassMixin, Replaceable):
id: str
sql: str
@dataclass
class CompiledNodeMixin(dbtClassMixin):
# this is a special mixin class to provide a required argument. If a node
# is missing a `compiled` flag entirely, it must not be a CompiledNode.
compiled: bool
@dataclass
class CompiledNode(ParsedNode, CompiledNodeMixin):
compiled_sql: Optional[str] = None
extra_ctes_injected: bool = False
extra_ctes: List[InjectedCTE] = field(default_factory=list)
relation_name: Optional[str] = None
_pre_injected_sql: Optional[str] = None
def set_cte(self, cte_id: str, sql: str):
"""This is the equivalent of what self.extra_ctes[cte_id] = sql would
do if extra_ctes were an OrderedDict
"""
for cte in self.extra_ctes:
if cte.id == cte_id:
cte.sql = sql
break
else:
self.extra_ctes.append(InjectedCTE(id=cte_id, sql=sql))
def __post_serialize__(self, dct):
dct = super().__post_serialize__(dct)
if "_pre_injected_sql" in dct:
del dct["_pre_injected_sql"]
return dct
@dataclass
class CompiledAnalysisNode(CompiledNode):
resource_type: NodeType = field(metadata={"restrict": [NodeType.Analysis]})
@dataclass
class CompiledHookNode(CompiledNode):
resource_type: NodeType = field(metadata={"restrict": [NodeType.Operation]})
index: Optional[int] = None
@dataclass
class CompiledModelNode(CompiledNode):
resource_type: NodeType = field(metadata={"restrict": [NodeType.Model]})
# TODO: rm?
@dataclass
class CompiledRPCNode(CompiledNode):
resource_type: NodeType = field(metadata={"restrict": [NodeType.RPCCall]})
@dataclass
class CompiledSqlNode(CompiledNode):
resource_type: NodeType = field(metadata={"restrict": [NodeType.SqlOperation]})
@dataclass
class CompiledSeedNode(CompiledNode):
# keep this in sync with ParsedSeedNode!
resource_type: NodeType = field(metadata={"restrict": [NodeType.Seed]})
config: SeedConfig = field(default_factory=SeedConfig)
@property
def empty(self):
"""Seeds are never empty"""
return False
def same_body(self, other) -> bool:
return same_seeds(self, other)
@dataclass
class CompiledSnapshotNode(CompiledNode):
resource_type: NodeType = field(metadata={"restrict": [NodeType.Snapshot]})
@dataclass
class CompiledSingularTestNode(CompiledNode):
resource_type: NodeType = field(metadata={"restrict": [NodeType.Test]})
# Was not able to make mypy happy and keep the code working. We need to
# refactor the various configs.
config: TestConfig = field(default_factory=TestConfig) # type:ignore
@dataclass
class CompiledGenericTestNode(CompiledNode, HasTestMetadata):
# keep this in sync with ParsedGenericTestNode!
resource_type: NodeType = field(metadata={"restrict": [NodeType.Test]})
column_name: Optional[str] = None
file_key_name: Optional[str] = None
# Was not able to make mypy happy and keep the code working. We need to
# refactor the various configs.
config: TestConfig = field(default_factory=TestConfig) # type:ignore
def same_contents(self, other) -> bool:
if other is None:
return False
return self.same_config(other) and self.same_fqn(other) and True
CompiledTestNode = Union[CompiledSingularTestNode, CompiledGenericTestNode]
PARSED_TYPES: Dict[Type[CompiledNode], Type[ParsedResource]] = {
CompiledAnalysisNode: ParsedAnalysisNode,
CompiledModelNode: ParsedModelNode,
CompiledHookNode: ParsedHookNode,
CompiledRPCNode: ParsedRPCNode,
CompiledSqlNode: ParsedSqlNode,
CompiledSeedNode: ParsedSeedNode,
CompiledSnapshotNode: ParsedSnapshotNode,
CompiledSingularTestNode: ParsedSingularTestNode,
CompiledGenericTestNode: ParsedGenericTestNode,
}
COMPILED_TYPES: Dict[Type[ParsedResource], Type[CompiledNode]] = {
ParsedAnalysisNode: CompiledAnalysisNode,
ParsedModelNode: CompiledModelNode,
ParsedHookNode: CompiledHookNode,
ParsedRPCNode: CompiledRPCNode,
ParsedSqlNode: CompiledSqlNode,
ParsedSeedNode: CompiledSeedNode,
ParsedSnapshotNode: CompiledSnapshotNode,
ParsedSingularTestNode: CompiledSingularTestNode,
ParsedGenericTestNode: CompiledGenericTestNode,
}
# for some types, the compiled type is the parsed type, so make this easy
CompiledType = Union[Type[CompiledNode], Type[ParsedResource]]
CompiledResource = Union[ParsedResource, CompiledNode]
def compiled_type_for(parsed: ParsedNode) -> CompiledType:
if type(parsed) in COMPILED_TYPES:
return COMPILED_TYPES[type(parsed)]
else:
return type(parsed)
def parsed_instance_for(compiled: CompiledNode) -> ParsedResource:
cls = PARSED_TYPES.get(type(compiled))
if cls is None:
# how???
raise ValueError("invalid resource_type: {}".format(compiled.resource_type))
return cls.from_dict(compiled.to_dict(omit_none=True))
NonSourceCompiledNode = Union[
CompiledAnalysisNode,
CompiledSingularTestNode,
CompiledModelNode,
CompiledHookNode,
CompiledRPCNode,
CompiledSqlNode,
CompiledGenericTestNode,
CompiledSeedNode,
CompiledSnapshotNode,
]
NonSourceParsedNode = Union[
ParsedAnalysisNode,
ParsedSingularTestNode,
ParsedHookNode,
ParsedModelNode,
ParsedRPCNode,
ParsedSqlNode,
ParsedGenericTestNode,
ParsedSeedNode,
ParsedSnapshotNode,
]
# This is anything that can be in manifest.nodes.
ManifestNode = Union[
NonSourceCompiledNode,
NonSourceParsedNode,
]
# We allow either parsed or compiled nodes, or parsed sources, as some
# 'compile()' calls in the runner actually just return the original parsed
# node they were given.
CompileResultNode = Union[
ManifestNode,
ParsedSourceDefinition,
]
# anything that participates in the graph: sources, exposures, metrics,
# or manifest nodes
GraphMemberNode = Union[
CompileResultNode,
ParsedExposure,
ParsedMetric,
]

View File

@@ -1,7 +1,7 @@
import enum import enum
from dataclasses import dataclass, field from dataclasses import dataclass, field
from itertools import chain, islice from itertools import chain, islice
from mashumaro.mixins.msgpack import DataClassMessagePackMixin from mashumaro import DataClassMessagePackMixin
from multiprocessing.synchronize import Lock from multiprocessing.synchronize import Lock
from typing import ( from typing import (
Dict, Dict,
@@ -16,40 +16,43 @@ from typing import (
TypeVar, TypeVar,
Callable, Callable,
Generic, Generic,
cast,
AbstractSet, AbstractSet,
ClassVar, ClassVar,
) )
from typing_extensions import Protocol from typing_extensions import Protocol
from uuid import UUID from uuid import UUID
from dbt.contracts.graph.nodes import ( from dbt.contracts.graph.compiled import (
Macro, CompileResultNode,
Documentation,
SourceDefinition,
GenericTestNode,
Exposure,
Metric,
Entity,
UnpatchedSourceDefinition,
ManifestNode, ManifestNode,
NonSourceCompiledNode,
GraphMemberNode, GraphMemberNode,
ResultNode, )
BaseNode, from dbt.contracts.graph.parsed import (
ParsedMacro,
ParsedDocumentation,
ParsedSourceDefinition,
ParsedExposure,
ParsedMetric,
HasUniqueID,
UnpatchedSourceDefinition,
ManifestNodes,
) )
from dbt.contracts.graph.unparsed import SourcePatch from dbt.contracts.graph.unparsed import SourcePatch
from dbt.contracts.files import SourceFile, SchemaSourceFile, FileHash, AnySourceFile from dbt.contracts.files import SourceFile, SchemaSourceFile, FileHash, AnySourceFile
from dbt.contracts.util import BaseArtifactMetadata, SourceKey, ArtifactMixin, schema_version from dbt.contracts.util import BaseArtifactMetadata, SourceKey, ArtifactMixin, schema_version
from dbt.dataclass_schema import dbtClassMixin from dbt.dataclass_schema import dbtClassMixin
from dbt.exceptions import ( from dbt.exceptions import (
CompilationError, CompilationException,
DuplicateResourceNameError, raise_duplicate_resource_name,
DuplicateMacroInPackageError, raise_compiler_error,
DuplicateMaterializationNameError,
) )
from dbt.helper_types import PathSet from dbt.helper_types import PathSet
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import MergedFromState from dbt.events.types import MergedFromState
from dbt.node_types import NodeType from dbt.node_types import NodeType
from dbt.ui import line_wrap_message
from dbt import flags from dbt import flags
from dbt import tracking from dbt import tracking
import dbt.utils import dbt.utils
@@ -92,7 +95,7 @@ class DocLookup(dbtClassMixin):
return self.perform_lookup(unique_id, manifest) return self.perform_lookup(unique_id, manifest)
return None return None
def add_doc(self, doc: Documentation): def add_doc(self, doc: ParsedDocumentation):
if doc.name not in self.storage: if doc.name not in self.storage:
self.storage[doc.name] = {} self.storage[doc.name] = {}
self.storage[doc.name][doc.package_name] = doc.unique_id self.storage[doc.name][doc.package_name] = doc.unique_id
@@ -101,9 +104,9 @@ class DocLookup(dbtClassMixin):
for doc in manifest.docs.values(): for doc in manifest.docs.values():
self.add_doc(doc) self.add_doc(doc)
def perform_lookup(self, unique_id: UniqueID, manifest) -> Documentation: def perform_lookup(self, unique_id: UniqueID, manifest) -> ParsedDocumentation:
if unique_id not in manifest.docs: if unique_id not in manifest.docs:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
f"Doc {unique_id} found in cache but not found in manifest" f"Doc {unique_id} found in cache but not found in manifest"
) )
return manifest.docs[unique_id] return manifest.docs[unique_id]
@@ -123,7 +126,7 @@ class SourceLookup(dbtClassMixin):
return self.perform_lookup(unique_id, manifest) return self.perform_lookup(unique_id, manifest)
return None return None
def add_source(self, source: SourceDefinition): def add_source(self, source: ParsedSourceDefinition):
if source.search_name not in self.storage: if source.search_name not in self.storage:
self.storage[source.search_name] = {} self.storage[source.search_name] = {}
@@ -134,9 +137,9 @@ class SourceLookup(dbtClassMixin):
if hasattr(source, "source_name"): if hasattr(source, "source_name"):
self.add_source(source) self.add_source(source)
def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> SourceDefinition: def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> ParsedSourceDefinition:
if unique_id not in manifest.sources: if unique_id not in manifest.sources:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
f"Source {unique_id} found in cache but not found in manifest" f"Source {unique_id} found in cache but not found in manifest"
) )
return manifest.sources[unique_id] return manifest.sources[unique_id]
@@ -174,79 +177,13 @@ class RefableLookup(dbtClassMixin):
def perform_lookup(self, unique_id: UniqueID, manifest) -> ManifestNode: def perform_lookup(self, unique_id: UniqueID, manifest) -> ManifestNode:
if unique_id not in manifest.nodes: if unique_id not in manifest.nodes:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
f"Node {unique_id} found in cache but not found in manifest" f"Node {unique_id} found in cache but not found in manifest"
) )
return manifest.nodes[unique_id] return manifest.nodes[unique_id]
class MetricLookup(dbtClassMixin): # This handles both models/seeds/snapshots and sources
def __init__(self, manifest: "Manifest"):
self.storage: Dict[str, Dict[PackageName, UniqueID]] = {}
self.populate(manifest)
def get_unique_id(self, search_name, package: Optional[PackageName]):
return find_unique_id_for_package(self.storage, search_name, package)
def find(self, search_name, package: Optional[PackageName], manifest: "Manifest"):
unique_id = self.get_unique_id(search_name, package)
if unique_id is not None:
return self.perform_lookup(unique_id, manifest)
return None
def add_metric(self, metric: Metric):
if metric.search_name not in self.storage:
self.storage[metric.search_name] = {}
self.storage[metric.search_name][metric.package_name] = metric.unique_id
def populate(self, manifest):
for metric in manifest.metrics.values():
if hasattr(metric, "name"):
self.add_metric(metric)
def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> Metric:
if unique_id not in manifest.metrics:
raise dbt.exceptions.DbtInternalError(
f"Metric {unique_id} found in cache but not found in manifest"
)
return manifest.metrics[unique_id]
class EntityLookup(dbtClassMixin):
def __init__(self, manifest: "Manifest"):
self.storage: Dict[str, Dict[PackageName, UniqueID]] = {}
self.populate(manifest)
def get_unique_id(self, search_name, package: Optional[PackageName]):
return find_unique_id_for_package(self.storage, search_name, package)
def find(self, search_name, package: Optional[PackageName], manifest: "Manifest"):
unique_id = self.get_unique_id(search_name, package)
if unique_id is not None:
return self.perform_lookup(unique_id, manifest)
return None
def add_entity(self, entity: Entity):
if entity.search_name not in self.storage:
self.storage[entity.search_name] = {}
self.storage[entity.search_name][entity.package_name] = entity.unique_id
def populate(self, manifest):
for entity in manifest.entities.values():
if hasattr(entity, "name"):
self.add_entity(entity)
def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> Entity:
if unique_id not in manifest.entities:
raise dbt.exceptions.DbtInternalError(
f"Entity {unique_id} found in cache but not found in manifest"
)
return manifest.entities[unique_id]
# This handles both models/seeds/snapshots and sources/metrics/exposures
class DisabledLookup(dbtClassMixin): class DisabledLookup(dbtClassMixin):
def __init__(self, manifest: "Manifest"): def __init__(self, manifest: "Manifest"):
self.storage: Dict[str, Dict[PackageName, List[Any]]] = {} self.storage: Dict[str, Dict[PackageName, List[Any]]] = {}
@@ -354,7 +291,7 @@ def _sort_values(dct):
def build_node_edges(nodes: List[ManifestNode]): def build_node_edges(nodes: List[ManifestNode]):
"""Build the forward and backward edges on the given list of ManifestNodes """Build the forward and backward edges on the given list of ParsedNodes
and return them as two separate dictionaries, each mapping unique IDs to and return them as two separate dictionaries, each mapping unique IDs to
lists of edges. lists of edges.
""" """
@@ -372,10 +309,10 @@ def build_node_edges(nodes: List[ManifestNode]):
# Build a map of children of macros and generic tests # Build a map of children of macros and generic tests
def build_macro_edges(nodes: List[Any]): def build_macro_edges(nodes: List[Any]):
forward_edges: Dict[str, List[str]] = { forward_edges: Dict[str, List[str]] = {
n.unique_id: [] for n in nodes if n.unique_id.startswith("macro") or n.depends_on_macros n.unique_id: [] for n in nodes if n.unique_id.startswith("macro") or n.depends_on.macros
} }
for node in nodes: for node in nodes:
for unique_id in node.depends_on_macros: for unique_id in node.depends_on.macros:
if unique_id in forward_edges.keys(): if unique_id in forward_edges.keys():
forward_edges[unique_id].append(node.unique_id) forward_edges[unique_id].append(node.unique_id)
return _sort_values(forward_edges) return _sort_values(forward_edges)
@@ -391,10 +328,15 @@ class Locality(enum.IntEnum):
Root = 3 Root = 3
class Specificity(enum.IntEnum):
Default = 1
Adapter = 2
@dataclass @dataclass
class MacroCandidate: class MacroCandidate:
locality: Locality locality: Locality
macro: Macro macro: ParsedMacro
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
if not isinstance(other, MacroCandidate): if not isinstance(other, MacroCandidate):
@@ -413,14 +355,12 @@ class MacroCandidate:
@dataclass @dataclass
class MaterializationCandidate(MacroCandidate): class MaterializationCandidate(MacroCandidate):
# specificity describes where in the inheritance chain this materialization candidate is specificity: Specificity
# a specificity of 0 means a materialization defined by the current adapter
# the highest the specificity describes a default materialization. the value itself depends on
# how many adapters there are in the inheritance chain
specificity: int
@classmethod @classmethod
def from_macro(cls, candidate: MacroCandidate, specificity: int) -> "MaterializationCandidate": def from_macro(
cls, candidate: MacroCandidate, specificity: Specificity
) -> "MaterializationCandidate":
return cls( return cls(
locality=candidate.locality, locality=candidate.locality,
macro=candidate.macro, macro=candidate.macro,
@@ -432,16 +372,21 @@ class MaterializationCandidate(MacroCandidate):
return NotImplemented return NotImplemented
equal = self.specificity == other.specificity and self.locality == other.locality equal = self.specificity == other.specificity and self.locality == other.locality
if equal: if equal:
raise DuplicateMaterializationNameError(self.macro, other) raise_compiler_error(
"Found two materializations with the name {} (packages {} and "
"{}). dbt cannot resolve this ambiguity".format(
self.macro.name, self.macro.package_name, other.macro.package_name
)
)
return equal return equal
def __lt__(self, other: object) -> bool: def __lt__(self, other: object) -> bool:
if not isinstance(other, MaterializationCandidate): if not isinstance(other, MaterializationCandidate):
return NotImplemented return NotImplemented
if self.specificity > other.specificity:
return True
if self.specificity < other.specificity: if self.specificity < other.specificity:
return True
if self.specificity > other.specificity:
return False return False
if self.locality < other.locality: if self.locality < other.locality:
return True return True
@@ -454,14 +399,16 @@ M = TypeVar("M", bound=MacroCandidate)
class CandidateList(List[M]): class CandidateList(List[M]):
def last(self) -> Optional[Macro]: def last(self) -> Optional[ParsedMacro]:
if not self: if not self:
return None return None
self.sort() self.sort()
return self[-1].macro return self[-1].macro
def _get_locality(macro: Macro, root_project_name: str, internal_packages: Set[str]) -> Locality: def _get_locality(
macro: ParsedMacro, root_project_name: str, internal_packages: Set[str]
) -> Locality:
if macro.package_name == root_project_name: if macro.package_name == root_project_name:
return Locality.Root return Locality.Root
elif macro.package_name in internal_packages: elif macro.package_name in internal_packages:
@@ -487,19 +434,13 @@ class Disabled(Generic[D]):
target: D target: D
MaybeMetricNode = Optional[Union[Metric, Disabled[Metric]]] MaybeDocumentation = Optional[ParsedDocumentation]
MaybeEntityNode = Optional[Union[Entity, Disabled[Entity]]]
MaybeDocumentation = Optional[Documentation]
MaybeParsedSource = Optional[ MaybeParsedSource = Optional[
Union[ Union[
SourceDefinition, ParsedSourceDefinition,
Disabled[SourceDefinition], Disabled[ParsedSourceDefinition],
] ]
] ]
@@ -517,14 +458,14 @@ def _update_into(dest: MutableMapping[str, T], new_item: T):
""" """
unique_id = new_item.unique_id unique_id = new_item.unique_id
if unique_id not in dest: if unique_id not in dest:
raise dbt.exceptions.DbtRuntimeError( raise dbt.exceptions.RuntimeException(
f"got an update_{new_item.resource_type} call with an " f"got an update_{new_item.resource_type} call with an "
f"unrecognized {new_item.resource_type}: {new_item.unique_id}" f"unrecognized {new_item.resource_type}: {new_item.unique_id}"
) )
existing = dest[unique_id] existing = dest[unique_id]
if new_item.original_file_path != existing.original_file_path: if new_item.original_file_path != existing.original_file_path:
raise dbt.exceptions.DbtRuntimeError( raise dbt.exceptions.RuntimeException(
f"cannot update a {new_item.resource_type} to have a new file path!" f"cannot update a {new_item.resource_type} to have a new file " f"path!"
) )
dest[unique_id] = new_item dest[unique_id] = new_item
@@ -539,7 +480,7 @@ class MacroMethods:
def find_macro_by_name( def find_macro_by_name(
self, name: str, root_project_name: str, package: Optional[str] self, name: str, root_project_name: str, package: Optional[str]
) -> Optional[Macro]: ) -> Optional[ParsedMacro]:
"""Find a macro in the graph by its name and package name, or None for """Find a macro in the graph by its name and package name, or None for
any package. The root project name is used to determine priority: any package. The root project name is used to determine priority:
- locally defined macros come first - locally defined macros come first
@@ -562,7 +503,7 @@ class MacroMethods:
def find_generate_macro_by_name( def find_generate_macro_by_name(
self, component: str, root_project_name: str self, component: str, root_project_name: str
) -> Optional[Macro]: ) -> Optional[ParsedMacro]:
""" """
The `generate_X_name` macros are similar to regular ones, but ignore The `generate_X_name` macros are similar to regular ones, but ignore
imported packages. imported packages.
@@ -631,19 +572,18 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
# is added it must all be added in the __reduce_ex__ method in the # is added it must all be added in the __reduce_ex__ method in the
# args tuple in the right position. # args tuple in the right position.
nodes: MutableMapping[str, ManifestNode] = field(default_factory=dict) nodes: MutableMapping[str, ManifestNode] = field(default_factory=dict)
sources: MutableMapping[str, SourceDefinition] = field(default_factory=dict) sources: MutableMapping[str, ParsedSourceDefinition] = field(default_factory=dict)
macros: MutableMapping[str, Macro] = field(default_factory=dict) macros: MutableMapping[str, ParsedMacro] = field(default_factory=dict)
docs: MutableMapping[str, Documentation] = field(default_factory=dict) docs: MutableMapping[str, ParsedDocumentation] = field(default_factory=dict)
exposures: MutableMapping[str, Exposure] = field(default_factory=dict) exposures: MutableMapping[str, ParsedExposure] = field(default_factory=dict)
metrics: MutableMapping[str, Metric] = field(default_factory=dict) metrics: MutableMapping[str, ParsedMetric] = field(default_factory=dict)
entities: MutableMapping[str, Entity] = field(default_factory=dict)
selectors: MutableMapping[str, Any] = field(default_factory=dict) selectors: MutableMapping[str, Any] = field(default_factory=dict)
files: MutableMapping[str, AnySourceFile] = field(default_factory=dict) files: MutableMapping[str, AnySourceFile] = field(default_factory=dict)
metadata: ManifestMetadata = field(default_factory=ManifestMetadata) metadata: ManifestMetadata = field(default_factory=ManifestMetadata)
flat_graph: Dict[str, Any] = field(default_factory=dict) flat_graph: Dict[str, Any] = field(default_factory=dict)
state_check: ManifestStateCheck = field(default_factory=ManifestStateCheck) state_check: ManifestStateCheck = field(default_factory=ManifestStateCheck)
source_patches: MutableMapping[SourceKey, SourcePatch] = field(default_factory=dict) source_patches: MutableMapping[SourceKey, SourcePatch] = field(default_factory=dict)
disabled: MutableMapping[str, List[GraphMemberNode]] = field(default_factory=dict) disabled: MutableMapping[str, List[CompileResultNode]] = field(default_factory=dict)
env_vars: MutableMapping[str, str] = field(default_factory=dict) env_vars: MutableMapping[str, str] = field(default_factory=dict)
_doc_lookup: Optional[DocLookup] = field( _doc_lookup: Optional[DocLookup] = field(
@@ -655,12 +595,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
_ref_lookup: Optional[RefableLookup] = field( _ref_lookup: Optional[RefableLookup] = field(
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None} default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
) )
_metric_lookup: Optional[MetricLookup] = field(
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
)
_entity_lookup: Optional[EntityLookup] = field(
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
)
_disabled_lookup: Optional[DisabledLookup] = field( _disabled_lookup: Optional[DisabledLookup] = field(
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None} default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
) )
@@ -687,7 +621,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
obj._lock = flags.MP_CONTEXT.Lock() obj._lock = flags.MP_CONTEXT.Lock()
return obj return obj
def sync_update_node(self, new_node: ManifestNode) -> ManifestNode: def sync_update_node(self, new_node: NonSourceCompiledNode) -> NonSourceCompiledNode:
"""update the node with a lock. The only time we should want to lock is """update the node with a lock. The only time we should want to lock is
when compiling an ephemeral ancestor of a node at runtime, because when compiling an ephemeral ancestor of a node at runtime, because
multiple threads could be just-in-time compiling the same ephemeral multiple threads could be just-in-time compiling the same ephemeral
@@ -700,24 +634,21 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
with self._lock: with self._lock:
existing = self.nodes[new_node.unique_id] existing = self.nodes[new_node.unique_id]
if getattr(existing, "compiled", False): if getattr(existing, "compiled", False):
# already compiled # already compiled -> must be a NonSourceCompiledNode
return existing return cast(NonSourceCompiledNode, existing)
_update_into(self.nodes, new_node) _update_into(self.nodes, new_node)
return new_node return new_node
def update_exposure(self, new_exposure: Exposure): def update_exposure(self, new_exposure: ParsedExposure):
_update_into(self.exposures, new_exposure) _update_into(self.exposures, new_exposure)
def update_metric(self, new_metric: Metric): def update_metric(self, new_metric: ParsedMetric):
_update_into(self.metrics, new_metric) _update_into(self.metrics, new_metric)
def update_entity(self, new_entity: Entity):
_update_into(self.entities, new_entity)
def update_node(self, new_node: ManifestNode): def update_node(self, new_node: ManifestNode):
_update_into(self.nodes, new_node) _update_into(self.nodes, new_node)
def update_source(self, new_source: SourceDefinition): def update_source(self, new_source: ParsedSourceDefinition):
_update_into(self.sources, new_source) _update_into(self.sources, new_source)
def build_flat_graph(self): def build_flat_graph(self):
@@ -729,7 +660,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self.flat_graph = { self.flat_graph = {
"exposures": {k: v.to_dict(omit_none=False) for k, v in self.exposures.items()}, "exposures": {k: v.to_dict(omit_none=False) for k, v in self.exposures.items()},
"metrics": {k: v.to_dict(omit_none=False) for k, v in self.metrics.items()}, "metrics": {k: v.to_dict(omit_none=False) for k, v in self.metrics.items()},
"entities": {k: v.to_dict(omit_none=False) for k, v in self.entities.items()},
"nodes": {k: v.to_dict(omit_none=False) for k, v in self.nodes.items()}, "nodes": {k: v.to_dict(omit_none=False) for k, v in self.nodes.items()},
"sources": {k: v.to_dict(omit_none=False) for k, v in self.sources.items()}, "sources": {k: v.to_dict(omit_none=False) for k, v in self.sources.items()},
} }
@@ -741,24 +671,18 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
disabled_by_file_id[node.file_id] = node disabled_by_file_id[node.file_id] = node
return disabled_by_file_id return disabled_by_file_id
def _get_parent_adapter_types(self, adapter_type: str) -> List[str]:
# This is duplicated logic from core/dbt/context/providers.py
# Ideally this would instead be incorporating actual dispatch logic
from dbt.adapters.factory import get_adapter_type_names
# order matters for dispatch:
# 1. current adapter
# 2. any parent adapters (dependencies)
# 3. 'default'
return get_adapter_type_names(adapter_type) + ["default"]
def _materialization_candidates_for( def _materialization_candidates_for(
self, self,
project_name: str, project_name: str,
materialization_name: str, materialization_name: str,
adapter_type: str, adapter_type: Optional[str],
specificity: int,
) -> CandidateList: ) -> CandidateList:
if adapter_type is None:
specificity = Specificity.Default
else:
specificity = Specificity.Adapter
full_name = dbt.utils.get_materialization_macro_name( full_name = dbt.utils.get_materialization_macro_name(
materialization_name=materialization_name, materialization_name=materialization_name,
adapter_type=adapter_type, adapter_type=adapter_type,
@@ -771,16 +695,15 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
def find_materialization_macro_by_name( def find_materialization_macro_by_name(
self, project_name: str, materialization_name: str, adapter_type: str self, project_name: str, materialization_name: str, adapter_type: str
) -> Optional[Macro]: ) -> Optional[ParsedMacro]:
candidates: CandidateList = CandidateList( candidates: CandidateList = CandidateList(
chain.from_iterable( chain.from_iterable(
self._materialization_candidates_for( self._materialization_candidates_for(
project_name=project_name, project_name=project_name,
materialization_name=materialization_name, materialization_name=materialization_name,
adapter_type=atype, adapter_type=atype,
specificity=specificity, # where in the inheritance chain this candidate is
) )
for specificity, atype in enumerate(self._get_parent_adapter_types(adapter_type)) for atype in (adapter_type, None)
) )
) )
return candidates.last() return candidates.last()
@@ -792,7 +715,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self.nodes.values(), self.nodes.values(),
self.sources.values(), self.sources.values(),
self.metrics.values(), self.metrics.values(),
self.entities.values(),
) )
for resource in all_resources: for resource in all_resources:
resource_type_plural = resource.resource_type.pluralize() resource_type_plural = resource.resource_type.pluralize()
@@ -814,22 +736,19 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
return frozenset(x.database for x in chain(self.nodes.values(), self.sources.values())) return frozenset(x.database for x in chain(self.nodes.values(), self.sources.values()))
def deepcopy(self): def deepcopy(self):
copy = Manifest( return Manifest(
nodes={k: _deepcopy(v) for k, v in self.nodes.items()}, nodes={k: _deepcopy(v) for k, v in self.nodes.items()},
sources={k: _deepcopy(v) for k, v in self.sources.items()}, sources={k: _deepcopy(v) for k, v in self.sources.items()},
macros={k: _deepcopy(v) for k, v in self.macros.items()}, macros={k: _deepcopy(v) for k, v in self.macros.items()},
docs={k: _deepcopy(v) for k, v in self.docs.items()}, docs={k: _deepcopy(v) for k, v in self.docs.items()},
exposures={k: _deepcopy(v) for k, v in self.exposures.items()}, exposures={k: _deepcopy(v) for k, v in self.exposures.items()},
metrics={k: _deepcopy(v) for k, v in self.metrics.items()}, metrics={k: _deepcopy(v) for k, v in self.metrics.items()},
entities={k: _deepcopy(v) for k, v in self.entities.items()},
selectors={k: _deepcopy(v) for k, v in self.selectors.items()}, selectors={k: _deepcopy(v) for k, v in self.selectors.items()},
metadata=self.metadata, metadata=self.metadata,
disabled={k: _deepcopy(v) for k, v in self.disabled.items()}, disabled={k: _deepcopy(v) for k, v in self.disabled.items()},
files={k: _deepcopy(v) for k, v in self.files.items()}, files={k: _deepcopy(v) for k, v in self.files.items()},
state_check=_deepcopy(self.state_check), state_check=_deepcopy(self.state_check),
) )
copy.build_flat_graph()
return copy
def build_parent_and_child_maps(self): def build_parent_and_child_maps(self):
edge_members = list( edge_members = list(
@@ -838,7 +757,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self.sources.values(), self.sources.values(),
self.exposures.values(), self.exposures.values(),
self.metrics.values(), self.metrics.values(),
self.entities.values(),
) )
) )
forward_edges, backward_edges = build_node_edges(edge_members) forward_edges, backward_edges = build_node_edges(edge_members)
@@ -864,7 +782,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
docs=self.docs, docs=self.docs,
exposures=self.exposures, exposures=self.exposures,
metrics=self.metrics, metrics=self.metrics,
entities=self.entities,
selectors=self.selectors, selectors=self.selectors,
metadata=self.metadata, metadata=self.metadata,
disabled=self.disabled, disabled=self.disabled,
@@ -886,11 +803,9 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
return self.exposures[unique_id] return self.exposures[unique_id]
elif unique_id in self.metrics: elif unique_id in self.metrics:
return self.metrics[unique_id] return self.metrics[unique_id]
elif unique_id in self.entities:
return self.entities[unique_id]
else: else:
# something terrible has happened # something terrible has happened
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
"Expected node {} not found in manifest".format(unique_id) "Expected node {} not found in manifest".format(unique_id)
) )
@@ -918,18 +833,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self._ref_lookup = RefableLookup(self) self._ref_lookup = RefableLookup(self)
return self._ref_lookup return self._ref_lookup
@property
def metric_lookup(self) -> MetricLookup:
if self._metric_lookup is None:
self._metric_lookup = MetricLookup(self)
return self._metric_lookup
@property
def entity_lookup(self) -> EntityLookup:
if self._entity_lookup is None:
self._entity_lookup = EntityLookup(self)
return self._entity_lookup
def rebuild_ref_lookup(self): def rebuild_ref_lookup(self):
self._ref_lookup = RefableLookup(self) self._ref_lookup = RefableLookup(self)
@@ -988,8 +891,8 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
search_name = f"{target_source_name}.{target_table_name}" search_name = f"{target_source_name}.{target_table_name}"
candidates = _search_packages(current_project, node_package) candidates = _search_packages(current_project, node_package)
source: Optional[SourceDefinition] = None source: Optional[ParsedSourceDefinition] = None
disabled: Optional[List[SourceDefinition]] = None disabled: Optional[List[ParsedSourceDefinition]] = None
for pkg in candidates: for pkg in candidates:
source = self.source_lookup.find(search_name, pkg, self) source = self.source_lookup.find(search_name, pkg, self)
@@ -1005,56 +908,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
return Disabled(disabled[0]) return Disabled(disabled[0])
return None return None
def resolve_metric(
self,
target_metric_name: str,
target_metric_package: Optional[str],
current_project: str,
node_package: str,
) -> MaybeMetricNode:
metric: Optional[Metric] = None
disabled: Optional[List[Metric]] = None
candidates = _search_packages(current_project, node_package, target_metric_package)
for pkg in candidates:
metric = self.metric_lookup.find(target_metric_name, pkg, self)
if metric is not None and metric.config.enabled:
return metric
# it's possible that the node is disabled
if disabled is None:
disabled = self.disabled_lookup.find(f"{target_metric_name}", pkg)
if disabled:
return Disabled(disabled[0])
return None
def resolve_entity(
self,
target_entity_name: str,
target_entity_package: Optional[str],
current_project: str,
node_package: str,
) -> MaybeEntityNode:
entity: Optional[Entity] = None
disabled: Optional[List[Entity]] = None
candidates = _search_packages(current_project, node_package, target_entity_package)
for pkg in candidates:
entity = self.entity_lookup.find(target_entity_name, pkg, self)
if entity is not None and entity.config.enabled:
return entity
# it's possible that the node is disabled
if disabled is None:
disabled = self.disabled_lookup.find(f"{target_entity_name}", pkg)
if disabled:
return Disabled(disabled[0])
return None
# Called by DocsRuntimeContext.doc # Called by DocsRuntimeContext.doc
def resolve_doc( def resolve_doc(
self, self,
@@ -1062,7 +915,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
package: Optional[str], package: Optional[str],
current_project: str, current_project: str,
node_package: str, node_package: str,
) -> Optional[Documentation]: ) -> Optional[ParsedDocumentation]:
"""Resolve the given documentation. This follows the same algorithm as """Resolve the given documentation. This follows the same algorithm as
resolve_ref except the is_enabled checks are unnecessary as docs are resolve_ref except the is_enabled checks are unnecessary as docs are
always enabled. always enabled.
@@ -1081,7 +934,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
adapter, adapter,
other: "WritableManifest", other: "WritableManifest",
selected: AbstractSet[UniqueID], selected: AbstractSet[UniqueID],
favor_state: bool = False,
) -> None: ) -> None:
"""Given the selected unique IDs and a writable manifest, update this """Given the selected unique IDs and a writable manifest, update this
manifest by replacing any unselected nodes with their counterpart. manifest by replacing any unselected nodes with their counterpart.
@@ -1096,28 +948,40 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
node.resource_type in refables node.resource_type in refables
and not node.is_ephemeral and not node.is_ephemeral
and unique_id not in selected and unique_id not in selected
and ( and not adapter.get_relation(current.database, current.schema, current.identifier)
not adapter.get_relation(current.database, current.schema, current.identifier)
or favor_state
)
): ):
merged.add(unique_id) merged.add(unique_id)
self.nodes[unique_id] = node.replace(deferred=True) self.nodes[unique_id] = node.replace(deferred=True)
# Rebuild the flat_graph, which powers the 'graph' context variable,
# now that we've deferred some nodes
self.build_flat_graph()
# log up to 5 items # log up to 5 items
sample = list(islice(merged, 5)) sample = list(islice(merged, 5))
fire_event(MergedFromState(num_merged=len(merged), sample=sample)) fire_event(MergedFromState(nbr_merged=len(merged), sample=sample))
# Methods that were formerly in ParseResult # Methods that were formerly in ParseResult
def add_macro(self, source_file: SourceFile, macro: Macro): def add_macro(self, source_file: SourceFile, macro: ParsedMacro):
if macro.unique_id in self.macros: if macro.unique_id in self.macros:
# detect that the macro exists and emit an error # detect that the macro exists and emit an error
raise DuplicateMacroInPackageError(macro=macro, macro_mapping=self.macros) other_path = self.macros[macro.unique_id].original_file_path
# subtract 2 for the "Compilation Error" indent
# note that the line wrap eats newlines, so if you want newlines,
# this is the result :(
msg = line_wrap_message(
f"""\
dbt found two macros named "{macro.name}" in the project
"{macro.package_name}".
To fix this error, rename or remove one of the following
macros:
- {macro.original_file_path}
- {other_path}
""",
subtract=2,
)
raise_compiler_error(msg)
self.macros[macro.unique_id] = macro self.macros[macro.unique_id] = macro
source_file.macros.append(macro.unique_id) source_file.macros.append(macro.unique_id)
@@ -1137,62 +1001,45 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self.sources[source.unique_id] = source # type: ignore self.sources[source.unique_id] = source # type: ignore
source_file.sources.append(source.unique_id) source_file.sources.append(source.unique_id)
def add_node_nofile(self, node: ManifestNode): def add_node_nofile(self, node: ManifestNodes):
# nodes can't be overwritten! # nodes can't be overwritten!
_check_duplicates(node, self.nodes) _check_duplicates(node, self.nodes)
self.nodes[node.unique_id] = node self.nodes[node.unique_id] = node
def add_node(self, source_file: AnySourceFile, node: ManifestNode, test_from=None): def add_node(self, source_file: AnySourceFile, node: ManifestNodes, test_from=None):
self.add_node_nofile(node) self.add_node_nofile(node)
if isinstance(source_file, SchemaSourceFile): if isinstance(source_file, SchemaSourceFile):
if isinstance(node, GenericTestNode):
assert test_from assert test_from
source_file.add_test(node.unique_id, test_from) source_file.add_test(node.unique_id, test_from)
if isinstance(node, Metric):
source_file.metrics.append(node.unique_id)
if isinstance(node, Exposure):
source_file.exposures.append(node.unique_id)
else: else:
source_file.nodes.append(node.unique_id) source_file.nodes.append(node.unique_id)
def add_exposure(self, source_file: SchemaSourceFile, exposure: Exposure): def add_exposure(self, source_file: SchemaSourceFile, exposure: ParsedExposure):
_check_duplicates(exposure, self.exposures) _check_duplicates(exposure, self.exposures)
self.exposures[exposure.unique_id] = exposure self.exposures[exposure.unique_id] = exposure
source_file.exposures.append(exposure.unique_id) source_file.exposures.append(exposure.unique_id)
def add_metric(self, source_file: SchemaSourceFile, metric: Metric): def add_metric(self, source_file: SchemaSourceFile, metric: ParsedMetric):
_check_duplicates(metric, self.metrics) _check_duplicates(metric, self.metrics)
self.metrics[metric.unique_id] = metric self.metrics[metric.unique_id] = metric
source_file.metrics.append(metric.unique_id) source_file.metrics.append(metric.unique_id)
def add_entity(self, source_file: SchemaSourceFile, entity: Entity): def add_disabled_nofile(self, node: CompileResultNode):
_check_duplicates(entity, self.entities)
self.entities[entity.unique_id] = entity
source_file.entities.append(entity.unique_id)
def add_disabled_nofile(self, node: GraphMemberNode):
# There can be multiple disabled nodes for the same unique_id # There can be multiple disabled nodes for the same unique_id
if node.unique_id in self.disabled: if node.unique_id in self.disabled:
self.disabled[node.unique_id].append(node) self.disabled[node.unique_id].append(node)
else: else:
self.disabled[node.unique_id] = [node] self.disabled[node.unique_id] = [node]
def add_disabled(self, source_file: AnySourceFile, node: ResultNode, test_from=None): def add_disabled(self, source_file: AnySourceFile, node: CompileResultNode, test_from=None):
self.add_disabled_nofile(node) self.add_disabled_nofile(node)
if isinstance(source_file, SchemaSourceFile): if isinstance(source_file, SchemaSourceFile):
if isinstance(node, GenericTestNode):
assert test_from assert test_from
source_file.add_test(node.unique_id, test_from) source_file.add_test(node.unique_id, test_from)
if isinstance(node, Metric):
source_file.metrics.append(node.unique_id)
if isinstance(node, Entity):
source_file.entities.append(node.unique_id)
if isinstance(node, Exposure):
source_file.exposures.append(node.unique_id)
else: else:
source_file.nodes.append(node.unique_id) source_file.nodes.append(node.unique_id)
def add_doc(self, source_file: SourceFile, doc: Documentation): def add_doc(self, source_file: SourceFile, doc: ParsedDocumentation):
_check_duplicates(doc, self.docs) _check_duplicates(doc, self.docs)
self.docs[doc.unique_id] = doc self.docs[doc.unique_id] = doc
source_file.docs.append(doc.unique_id) source_file.docs.append(doc.unique_id)
@@ -1214,7 +1061,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self.docs, self.docs,
self.exposures, self.exposures,
self.metrics, self.metrics,
self.entities,
self.selectors, self.selectors,
self.files, self.files,
self.metadata, self.metadata,
@@ -1226,8 +1072,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self._doc_lookup, self._doc_lookup,
self._source_lookup, self._source_lookup,
self._ref_lookup, self._ref_lookup,
self._metric_lookup,
self._entity_lookup,
self._disabled_lookup, self._disabled_lookup,
self._analysis_lookup, self._analysis_lookup,
) )
@@ -1247,35 +1091,32 @@ AnyManifest = Union[Manifest, MacroManifest]
@dataclass @dataclass
@schema_version("manifest", 8) @schema_version("manifest", 5)
class WritableManifest(ArtifactMixin): class WritableManifest(ArtifactMixin):
nodes: Mapping[UniqueID, ManifestNode] = field( nodes: Mapping[UniqueID, ManifestNode] = field(
metadata=dict(description=("The nodes defined in the dbt project and its dependencies")) metadata=dict(description=("The nodes defined in the dbt project and its dependencies"))
) )
sources: Mapping[UniqueID, SourceDefinition] = field( sources: Mapping[UniqueID, ParsedSourceDefinition] = field(
metadata=dict(description=("The sources defined in the dbt project and its dependencies")) metadata=dict(description=("The sources defined in the dbt project and its dependencies"))
) )
macros: Mapping[UniqueID, Macro] = field( macros: Mapping[UniqueID, ParsedMacro] = field(
metadata=dict(description=("The macros defined in the dbt project and its dependencies")) metadata=dict(description=("The macros defined in the dbt project and its dependencies"))
) )
docs: Mapping[UniqueID, Documentation] = field( docs: Mapping[UniqueID, ParsedDocumentation] = field(
metadata=dict(description=("The docs defined in the dbt project and its dependencies")) metadata=dict(description=("The docs defined in the dbt project and its dependencies"))
) )
exposures: Mapping[UniqueID, Exposure] = field( exposures: Mapping[UniqueID, ParsedExposure] = field(
metadata=dict( metadata=dict(
description=("The exposures defined in the dbt project and its dependencies") description=("The exposures defined in the dbt project and its dependencies")
) )
) )
metrics: Mapping[UniqueID, Metric] = field( metrics: Mapping[UniqueID, ParsedMetric] = field(
metadata=dict(description=("The metrics defined in the dbt project and its dependencies")) metadata=dict(description=("The metrics defined in the dbt project and its dependencies"))
) )
entities: Mapping[UniqueID, Entity] = field(
metadata=dict(description=("The entities defined in the dbt project and its dependencies"))
)
selectors: Mapping[UniqueID, Any] = field( selectors: Mapping[UniqueID, Any] = field(
metadata=dict(description=("The selectors defined in selectors.yml")) metadata=dict(description=("The selectors defined in selectors.yml"))
) )
disabled: Optional[Mapping[UniqueID, List[ResultNode]]] = field( disabled: Optional[Mapping[UniqueID, List[CompileResultNode]]] = field(
metadata=dict(description="A mapping of the disabled nodes in the target") metadata=dict(description="A mapping of the disabled nodes in the target")
) )
parent_map: Optional[NodeEdgeMap] = field( parent_map: Optional[NodeEdgeMap] = field(
@@ -1294,21 +1135,10 @@ class WritableManifest(ArtifactMixin):
) )
) )
@classmethod
def compatible_previous_versions(self):
# return [("manifest", 4), ("manifest", 5), ("manifest", 6), ("manifest", 7)]
return []
def __post_serialize__(self, dct): def _check_duplicates(value: HasUniqueID, src: Mapping[str, HasUniqueID]):
for unique_id, node in dct["nodes"].items():
if "config_call_dict" in node:
del node["config_call_dict"]
return dct
def _check_duplicates(value: BaseNode, src: Mapping[str, BaseNode]):
if value.unique_id in src: if value.unique_id in src:
raise DuplicateResourceNameError(value, src[value.unique_id]) raise_duplicate_resource_name(value, src[value.unique_id])
K_T = TypeVar("K_T") K_T = TypeVar("K_T")
@@ -1317,7 +1147,7 @@ V_T = TypeVar("V_T")
def _expect_value(key: K_T, src: Mapping[K_T, V_T], old_file: SourceFile, name: str) -> V_T: def _expect_value(key: K_T, src: Mapping[K_T, V_T], old_file: SourceFile, name: str) -> V_T:
if key not in src: if key not in src:
raise CompilationError( raise CompilationException(
'Expected to find "{}" in cached "result.{}" based ' 'Expected to find "{}" in cached "result.{}" based '
"on cached file information: {}!".format(key, name, old_file) "on cached file information: {}!".format(key, name, old_file)
) )

View File

@@ -1,93 +0,0 @@
from dbt.node_types import NodeType
class MetricReference(object):
def __init__(self, metric_name, package_name=None):
self.metric_name = metric_name
self.package_name = package_name
def __str__(self):
return f"{self.metric_name}"
class ResolvedMetricReference(MetricReference):
"""
Simple proxy over a Metric which delegates property
lookups to the underlying node. Also adds helper functions
for working with metrics (ie. __str__ and templating functions)
"""
def __init__(self, node, manifest, Relation):
super().__init__(node.name, node.package_name)
self.node = node
self.manifest = manifest
self.Relation = Relation
def __getattr__(self, key):
return getattr(self.node, key)
def __str__(self):
return f"{self.node.name}"
@classmethod
def parent_metrics(cls, metric_node, manifest):
yield metric_node
for parent_unique_id in metric_node.depends_on.nodes:
node = manifest.metrics.get(parent_unique_id)
if node and node.resource_type == NodeType.Metric:
yield from cls.parent_metrics(node, manifest)
@classmethod
def parent_metrics_names(cls, metric_node, manifest):
yield metric_node.name
for parent_unique_id in metric_node.depends_on.nodes:
node = manifest.metrics.get(parent_unique_id)
if node and node.resource_type == NodeType.Metric:
yield from cls.parent_metrics_names(node, manifest)
@classmethod
def reverse_dag_parsing(cls, metric_node, manifest, metric_depth_count):
if metric_node.calculation_method == "derived":
yield {metric_node.name: metric_depth_count}
metric_depth_count = metric_depth_count + 1
for parent_unique_id in metric_node.depends_on.nodes:
node = manifest.metrics.get(parent_unique_id)
if (
node
and node.resource_type == NodeType.Metric
and node.calculation_method == "derived"
):
yield from cls.reverse_dag_parsing(node, manifest, metric_depth_count)
def full_metric_dependency(self):
to_return = list(set(self.parent_metrics_names(self.node, self.manifest)))
return to_return
def base_metric_dependency(self):
in_scope_metrics = list(self.parent_metrics(self.node, self.manifest))
to_return = []
for metric in in_scope_metrics:
if metric.calculation_method != "derived" and metric.name not in to_return:
to_return.append(metric.name)
return to_return
def derived_metric_dependency(self):
in_scope_metrics = list(self.parent_metrics(self.node, self.manifest))
to_return = []
for metric in in_scope_metrics:
if metric.calculation_method == "derived" and metric.name not in to_return:
to_return.append(metric.name)
return to_return
def derived_metric_dependency_depth(self):
metric_depth_count = 1
to_return = list(self.reverse_dag_parsing(self.node, self.manifest, metric_depth_count))
return to_return

View File

@@ -7,9 +7,8 @@ from dbt.dataclass_schema import (
ValidationError, ValidationError,
register_pattern, register_pattern,
) )
from dbt.contracts.graph.unparsed import AdditionalPropertiesAllowed, Docs from dbt.contracts.graph.unparsed import AdditionalPropertiesAllowed
from dbt.contracts.graph.utils import validate_color from dbt.exceptions import InternalException, CompilationException
from dbt.exceptions import DbtInternalError, CompilationError
from dbt.contracts.util import Replaceable, list_str from dbt.contracts.util import Replaceable, list_str
from dbt import hooks from dbt import hooks
from dbt.node_types import NodeType from dbt.node_types import NodeType
@@ -30,7 +29,7 @@ def _get_meta_value(cls: Type[M], fld: Field, key: str, default: Any) -> M:
try: try:
return cls(value) return cls(value)
except ValueError as exc: except ValueError as exc:
raise DbtInternalError(f"Invalid {cls} value: {value}") from exc raise InternalException(f"Invalid {cls} value: {value}") from exc
def _set_meta_value(obj: M, key: str, existing: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: def _set_meta_value(obj: M, key: str, existing: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
@@ -67,7 +66,6 @@ class MergeBehavior(Metadata):
Append = 1 Append = 1
Update = 2 Update = 2
Clobber = 3 Clobber = 3
DictKeyAppend = 4
@classmethod @classmethod
def default_field(cls) -> "MergeBehavior": def default_field(cls) -> "MergeBehavior":
@@ -126,9 +124,6 @@ def _listify(value: Any) -> List:
return [value] return [value]
# There are two versions of this code. The one here is for config
# objects, the one in _add_config_call in context_config.py is for
# config_call_dict dictionaries.
def _merge_field_value( def _merge_field_value(
merge_behavior: MergeBehavior, merge_behavior: MergeBehavior,
self_value: Any, self_value: Any,
@@ -140,39 +135,14 @@ def _merge_field_value(
return _listify(self_value) + _listify(other_value) return _listify(self_value) + _listify(other_value)
elif merge_behavior == MergeBehavior.Update: elif merge_behavior == MergeBehavior.Update:
if not isinstance(self_value, dict): if not isinstance(self_value, dict):
raise DbtInternalError(f"expected dict, got {self_value}") raise InternalException(f"expected dict, got {self_value}")
if not isinstance(other_value, dict): if not isinstance(other_value, dict):
raise DbtInternalError(f"expected dict, got {other_value}") raise InternalException(f"expected dict, got {other_value}")
value = self_value.copy() value = self_value.copy()
value.update(other_value) value.update(other_value)
return value return value
elif merge_behavior == MergeBehavior.DictKeyAppend:
if not isinstance(self_value, dict):
raise DbtInternalError(f"expected dict, got {self_value}")
if not isinstance(other_value, dict):
raise DbtInternalError(f"expected dict, got {other_value}")
new_dict = {}
for key in self_value.keys():
new_dict[key] = _listify(self_value[key])
for key in other_value.keys():
extend = False
new_key = key
# This might start with a +, to indicate we should extend the list
# instead of just clobbering it
if new_key.startswith("+"):
new_key = key.lstrip("+")
extend = True
if new_key in new_dict and extend:
# extend the list
value = other_value[key]
new_dict[new_key].extend(_listify(value))
else: else:
# clobber the list raise InternalException(f"Got an invalid merge_behavior: {merge_behavior}")
new_dict[new_key] = _listify(other_value[key])
return new_dict
else:
raise DbtInternalError(f"Got an invalid merge_behavior: {merge_behavior}")
def insensitive_patterns(*patterns: str): def insensitive_patterns(*patterns: str):
@@ -227,7 +197,7 @@ class BaseConfig(AdditionalPropertiesAllowed, Replaceable):
msg = ( msg = (
'Error, tried to delete config key "{}": Cannot delete ' "built-in keys" 'Error, tried to delete config key "{}": Cannot delete ' "built-in keys"
).format(key) ).format(key)
raise CompilationError(msg) raise CompilationException(msg)
else: else:
del self._extra[key] del self._extra[key]
@@ -286,8 +256,7 @@ class BaseConfig(AdditionalPropertiesAllowed, Replaceable):
# 'meta' moved here from node # 'meta' moved here from node
mergebehavior = { mergebehavior = {
"append": ["pre-hook", "pre_hook", "post-hook", "post_hook", "tags"], "append": ["pre-hook", "pre_hook", "post-hook", "post_hook", "tags"],
"update": ["quoting", "column_types", "meta", "docs"], "update": ["quoting", "column_types", "meta"],
"dict_key_append": ["grants"],
} }
@classmethod @classmethod
@@ -363,24 +332,43 @@ class BaseConfig(AdditionalPropertiesAllowed, Replaceable):
return self.from_dict(dct) return self.from_dict(dct)
@dataclass
class MetricConfig(BaseConfig):
enabled: bool = True
@dataclass
class EntityConfig(BaseConfig):
enabled: bool = True
@dataclass
class ExposureConfig(BaseConfig):
enabled: bool = True
@dataclass @dataclass
class SourceConfig(BaseConfig): class SourceConfig(BaseConfig):
enabled: bool = True enabled: bool = True
# to be implmented to complete CT-201
# quoting: Dict[str, Any] = field(
# default_factory=dict,
# metadata=MergeBehavior.Update.meta(),
# )
# freshness: Optional[Dict[str, Any]] = field(
# default=None,
# metadata=CompareBehavior.Exclude.meta(),
# )
# loader: Optional[str] = field(
# default=None,
# metadata=CompareBehavior.Exclude.meta(),
# )
# # TODO what type is this? docs say: "<column_name_or_expression>"
# loaded_at_field: Optional[str] = field(
# default=None,
# metadata=CompareBehavior.Exclude.meta(),
# )
# database: Optional[str] = field(
# default=None,
# metadata=CompareBehavior.Exclude.meta(),
# )
# schema: Optional[str] = field(
# default=None,
# metadata=CompareBehavior.Exclude.meta(),
# )
# meta: Dict[str, Any] = field(
# default_factory=dict,
# metadata=MergeBehavior.Update.meta(),
# )
# tags: Union[List[str], str] = field(
# default_factory=list_str,
# metadata=metas(ShowBehavior.Hide, MergeBehavior.Append, CompareBehavior.Exclude),
# )
@dataclass @dataclass
@@ -415,7 +403,6 @@ class NodeConfig(NodeAndTestConfig):
# Note: if any new fields are added with MergeBehavior, also update the # Note: if any new fields are added with MergeBehavior, also update the
# 'mergebehavior' dictionary # 'mergebehavior' dictionary
materialized: str = "view" materialized: str = "view"
incremental_strategy: Optional[str] = None
persist_docs: Dict[str, Any] = field(default_factory=dict) persist_docs: Dict[str, Any] = field(default_factory=dict)
post_hook: List[Hook] = field( post_hook: List[Hook] = field(
default_factory=list, default_factory=list,
@@ -440,27 +427,6 @@ class NodeConfig(NodeAndTestConfig):
# sometimes getting the Union order wrong, causing serialization failures. # sometimes getting the Union order wrong, causing serialization failures.
unique_key: Union[str, List[str], None] = None unique_key: Union[str, List[str], None] = None
on_schema_change: Optional[str] = "ignore" on_schema_change: Optional[str] = "ignore"
grants: Dict[str, Any] = field(
default_factory=dict, metadata=MergeBehavior.DictKeyAppend.meta()
)
packages: List[str] = field(
default_factory=list,
metadata=MergeBehavior.Append.meta(),
)
docs: Docs = field(
default_factory=Docs,
metadata=MergeBehavior.Update.meta(),
)
# we validate that node_color has a suitable value to prevent dbt-docs from crashing
def __post_init__(self):
if self.docs.node_color:
node_color = self.docs.node_color
if not validate_color(node_color):
raise ValidationError(
f"Invalid color name for docs.node_color: {node_color}. "
"It is neither a valid HTML color name nor a valid HEX code."
)
@classmethod @classmethod
def __pre_deserialize__(cls, data): def __pre_deserialize__(cls, data):
@@ -500,12 +466,6 @@ class SeedConfig(NodeConfig):
materialized: str = "seed" materialized: str = "seed"
quote_columns: Optional[bool] = None quote_columns: Optional[bool] = None
@classmethod
def validate(cls, data):
super().validate(data)
if data.get("materialized") and data.get("materialized") != "seed":
raise ValidationError("A seed must have a materialized value of 'seed'")
@dataclass @dataclass
class TestConfig(NodeAndTestConfig): class TestConfig(NodeAndTestConfig):
@@ -545,12 +505,6 @@ class TestConfig(NodeAndTestConfig):
return False return False
return True return True
@classmethod
def validate(cls, data):
super().validate(data)
if data.get("materialized") and data.get("materialized") != "test":
raise ValidationError("A test must have a materialized value of 'test'")
@dataclass @dataclass
class EmptySnapshotConfig(NodeConfig): class EmptySnapshotConfig(NodeConfig):
@@ -587,6 +541,7 @@ class SnapshotConfig(EmptySnapshotConfig):
f"Invalid value for 'check_cols': {data['check_cols']}. " f"Invalid value for 'check_cols': {data['check_cols']}. "
"Expected 'all' or a list of strings." "Expected 'all' or a list of strings."
) )
elif data.get("strategy") == "timestamp": elif data.get("strategy") == "timestamp":
if not data.get("updated_at"): if not data.get("updated_at"):
raise ValidationError( raise ValidationError(
@@ -598,9 +553,6 @@ class SnapshotConfig(EmptySnapshotConfig):
# If the strategy is not 'check' or 'timestamp' it's a custom strategy, # If the strategy is not 'check' or 'timestamp' it's a custom strategy,
# formerly supported with GenericSnapshotConfig # formerly supported with GenericSnapshotConfig
if data.get("materialized") and data.get("materialized") != "snapshot":
raise ValidationError("A snapshot must have a materialized value of 'snapshot'")
def finalize_and_validate(self): def finalize_and_validate(self):
data = self.to_dict(omit_none=True) data = self.to_dict(omit_none=True)
self.validate(data) self.validate(data)
@@ -608,9 +560,6 @@ class SnapshotConfig(EmptySnapshotConfig):
RESOURCE_TYPES: Dict[NodeType, Type[BaseConfig]] = { RESOURCE_TYPES: Dict[NodeType, Type[BaseConfig]] = {
NodeType.Metric: MetricConfig,
NodeType.Entity: EntityConfig,
NodeType.Exposure: ExposureConfig,
NodeType.Source: SourceConfig, NodeType.Source: SourceConfig,
NodeType.Seed: SeedConfig, NodeType.Seed: SeedConfig,
NodeType.Test: TestConfig, NodeType.Test: TestConfig,

View File

@@ -1,19 +1,15 @@
import re
from dbt import deprecations
from dbt.node_types import NodeType from dbt.node_types import NodeType
from dbt.contracts.util import ( from dbt.contracts.util import (
AdditionalPropertiesMixin, AdditionalPropertiesMixin,
Mergeable, Mergeable,
Replaceable, Replaceable,
rename_metric_attr,
) )
# trigger the PathEncoder # trigger the PathEncoder
import dbt.helper_types # noqa:F401 import dbt.helper_types # noqa:F401
from dbt.exceptions import CompilationError, ParsingError from dbt.exceptions import CompilationException
from dbt.dataclass_schema import dbtClassMixin, StrEnum, ExtensibleDbtClassMixin, ValidationError from dbt.dataclass_schema import dbtClassMixin, StrEnum, ExtensibleDbtClassMixin
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
@@ -24,6 +20,7 @@ from typing import Optional, List, Union, Dict, Any, Sequence
@dataclass @dataclass
class UnparsedBaseNode(dbtClassMixin, Replaceable): class UnparsedBaseNode(dbtClassMixin, Replaceable):
package_name: str package_name: str
root_path: str
path: str path: str
original_file_path: str original_file_path: str
@@ -33,27 +30,26 @@ class UnparsedBaseNode(dbtClassMixin, Replaceable):
@dataclass @dataclass
class HasCode(dbtClassMixin): class HasSQL:
raw_code: str raw_sql: str
language: str
@property @property
def empty(self): def empty(self):
return not self.raw_code.strip() return not self.raw_sql.strip()
@dataclass @dataclass
class UnparsedMacro(UnparsedBaseNode, HasCode): class UnparsedMacro(UnparsedBaseNode, HasSQL):
resource_type: NodeType = field(metadata={"restrict": [NodeType.Macro]}) resource_type: NodeType = field(metadata={"restrict": [NodeType.Macro]})
@dataclass @dataclass
class UnparsedGenericTest(UnparsedBaseNode, HasCode): class UnparsedGenericTest(UnparsedBaseNode, HasSQL):
resource_type: NodeType = field(metadata={"restrict": [NodeType.Macro]}) resource_type: NodeType = field(metadata={"restrict": [NodeType.Macro]})
@dataclass @dataclass
class UnparsedNode(UnparsedBaseNode, HasCode): class UnparsedNode(UnparsedBaseNode, HasSQL):
name: str name: str
resource_type: NodeType = field( resource_type: NodeType = field(
metadata={ metadata={
@@ -84,7 +80,6 @@ class UnparsedRunHook(UnparsedNode):
@dataclass @dataclass
class Docs(dbtClassMixin, Replaceable): class Docs(dbtClassMixin, Replaceable):
show: bool = True show: bool = True
node_color: Optional[str] = None
@dataclass @dataclass
@@ -222,7 +217,7 @@ class ExternalPartition(AdditionalPropertiesAllowed, Replaceable):
def __post_init__(self): def __post_init__(self):
if self.name == "" or self.data_type == "": if self.name == "" or self.data_type == "":
raise CompilationError("External partition columns must have names and data types") raise CompilationException("External partition columns must have names and data types")
@dataclass @dataclass
@@ -231,7 +226,7 @@ class ExternalTable(AdditionalPropertiesAllowed, Mergeable):
file_format: Optional[str] = None file_format: Optional[str] = None
row_format: Optional[str] = None row_format: Optional[str] = None
tbl_properties: Optional[str] = None tbl_properties: Optional[str] = None
partitions: Optional[Union[List[str], List[ExternalPartition]]] = None partitions: Optional[List[ExternalPartition]] = None
def __bool__(self): def __bool__(self):
return self.location is not None return self.location is not None
@@ -363,6 +358,7 @@ class SourcePatch(dbtClassMixin, Replaceable):
@dataclass @dataclass
class UnparsedDocumentation(dbtClassMixin, Replaceable): class UnparsedDocumentation(dbtClassMixin, Replaceable):
package_name: str package_name: str
root_path: str
path: str path: str
original_file_path: str original_file_path: str
@@ -435,21 +431,11 @@ class UnparsedExposure(dbtClassMixin, Replaceable):
type: ExposureType type: ExposureType
owner: ExposureOwner owner: ExposureOwner
description: str = "" description: str = ""
label: Optional[str] = None
maturity: Optional[MaturityType] = None maturity: Optional[MaturityType] = None
meta: Dict[str, Any] = field(default_factory=dict) meta: Dict[str, Any] = field(default_factory=dict)
tags: List[str] = field(default_factory=list) tags: List[str] = field(default_factory=list)
url: Optional[str] = None url: Optional[str] = None
depends_on: List[str] = field(default_factory=list) depends_on: List[str] = field(default_factory=list)
config: Dict[str, Any] = field(default_factory=dict)
@classmethod
def validate(cls, data):
super(UnparsedExposure, cls).validate(data)
if "name" in data:
# name can only contain alphanumeric chars and underscores
if not (re.match(r"[\w-]+$", data["name"])):
deprecations.warn("exposure-name", exposure=data["name"])
@dataclass @dataclass
@@ -460,94 +446,17 @@ class MetricFilter(dbtClassMixin, Replaceable):
value: str value: str
class MetricTimePeriod(StrEnum):
day = "day"
week = "week"
month = "month"
year = "year"
def plural(self) -> str:
return str(self) + "s"
@dataclass
class MetricTime(dbtClassMixin, Mergeable):
count: Optional[int] = None
period: Optional[MetricTimePeriod] = None
def __bool__(self):
return self.count is not None and self.period is not None
@dataclass @dataclass
class UnparsedMetric(dbtClassMixin, Replaceable): class UnparsedMetric(dbtClassMixin, Replaceable):
model: str
name: str name: str
label: str label: str
calculation_method: str type: str
expression: str
description: str = "" description: str = ""
sql: Optional[str] = None
timestamp: Optional[str] = None timestamp: Optional[str] = None
time_grains: List[str] = field(default_factory=list) time_grains: List[str] = field(default_factory=list)
dimensions: List[str] = field(default_factory=list) dimensions: List[str] = field(default_factory=list)
window: Optional[MetricTime] = None
model: Optional[str] = None
filters: List[MetricFilter] = field(default_factory=list) filters: List[MetricFilter] = field(default_factory=list)
meta: Dict[str, Any] = field(default_factory=dict) meta: Dict[str, Any] = field(default_factory=dict)
tags: List[str] = field(default_factory=list) tags: List[str] = field(default_factory=list)
config: Dict[str, Any] = field(default_factory=dict)
@classmethod
def validate(cls, data):
data = rename_metric_attr(data, raise_deprecation_warning=True)
super(UnparsedMetric, cls).validate(data)
if "name" in data:
errors = []
if " " in data["name"]:
errors.append("cannot contain spaces")
# This handles failing queries due to too long metric names.
# It only occurs in BigQuery and Snowflake (Postgres/Redshift truncate)
if len(data["name"]) > 250:
errors.append("cannot contain more than 250 characters")
if not (re.match(r"^[A-Za-z]", data["name"])):
errors.append("must begin with a letter")
if not (re.match(r"[\w-]+$", data["name"])):
errors.append("must contain only letters, numbers and underscores")
if errors:
raise ParsingError(
f"The metric name '{data['name']}' is invalid. It {', '.join(e for e in errors)}"
)
if data.get("timestamp") is None and data.get("time_grains") is not None:
raise ValidationError(
f"The metric '{data['name']} has time_grains defined but is missing a timestamp dimension."
)
if data.get("timestamp") is None and data.get("window") is not None:
raise ValidationError(
f"The metric '{data['name']} has a window defined but is missing a timestamp dimension."
)
if data.get("model") is None and data.get("calculation_method") != "derived":
raise ValidationError("Non-derived metrics require a 'model' property")
if data.get("model") is not None and data.get("calculation_method") == "derived":
raise ValidationError("Derived metrics cannot have a 'model' property")
@dataclass
class UnparsedEntity(dbtClassMixin, Replaceable):
"""This class is used for entity information"""
name: str
model: str
description: str = ""
dimensions: List[str] = field(default_factory=list)
meta: Dict[str, Any] = field(default_factory=dict)
tags: List[str] = field(default_factory=list)
config: Dict[str, Any] = field(default_factory=dict)
@classmethod
def validate(cls, data):
super(UnparsedEntity, cls).validate(data)
# TODO: Add validation here around include/exclude and others

Some files were not shown because too many files have changed in this diff Show More