Compare commits

...

1812 Commits

Author SHA1 Message Date
Kyle Wigley
03dfb11d2b wait for successful connection before setting up db 2021-09-13 11:43:17 -04:00
Kyle Wigley
3effade266 deepcopy args when passed down to rpc pask (#3850) 2021-09-10 16:34:57 -04:00
Jeremy Cohen
44e7390526 Specify macro_namespace of global_project dispatch macros (#3851)
* Specify macro_namespace of global_project dispatch macros

* Dispatch get_custom_alias, too

* Add integration tests

* Add changelog entry
2021-09-09 12:59:39 +02:00
Jeremy Cohen
c141798abc Use a macro for where subquery in tests (#3859)
* Use a macro for where subquery in tests

* Fix existing tests

* Add test case reproducing #3857

* Add changelog entry
2021-09-09 12:38:09 +02:00
sungchun12
df7ec3fb37 Fix dbt deps sorting behavior for non-standard version semantics (#3856)
* robust sorting and default to original version str

* more unique version semantics

* add another non-standard version example

* fix mypy issue
2021-09-09 12:13:09 +02:00
AndreasTA-AW
90e5507d03 #3682 Changed how tables and views are generated to be able to use differen… (#3691)
* Changed how tables and views are generated to be able to use different options

* 3682 added unit tests

* 3682 had conflict in changelog and became a bit messy

* 3682 Tested to add default kms to dataset and accidently pushed the changes
2021-09-09 12:01:02 +02:00
leahwicz
332d3494b3 Adding ADR directory, guidelines, first one (#3844)
* Adding ADR README

* Adding perf testing ADR

* fill in adr sections

Co-authored-by: Nathaniel May <nathaniel.may@fishtownanalytics.com>
2021-09-07 14:05:21 -04:00
Anna Filippova
6393f5a5d7 Feature: Add support for Package name changes on the Hub (#3825)
* Add warning about new package name

* Update CHANGELOG.md

* make linter happy

* Add warning about new package name

* Update CHANGELOG.md

* make linter happy

* move warnings to deprecations

* Update core/dbt/clients/registry.py

Co-authored-by: leahwicz <60146280+leahwicz@users.noreply.github.com>

* add comments for posterity

* Update core/dbt/deprecations.py

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>

* add deprecation test

Co-authored-by: leahwicz <60146280+leahwicz@users.noreply.github.com>
Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-09-07 11:53:02 -04:00
Kyle Wigley
ce97a9ca7a explicitly define __init__ for Profile class (#3855) 2021-09-07 10:38:51 -04:00
Jeremy Cohen
9af071bfe4 Add adapter_unique_id to invocation tracking (#3796)
* Add properties + methods for adapter_unique_id

* Turn on tracking
2021-09-03 16:38:20 +02:00
sungchun12
45a41202f3 fix prerelease imports with loose version semantics (#3852)
* fix prereleases imports with looseversion

* update for non-standard versions
2021-09-02 17:52:36 +02:00
Kyle Wigley
9768999ca1 Only set single_threaded for the rpc list task, not the cli list task (#3848) 2021-09-02 10:11:39 -04:00
juma-adoreme
fc0d11c0a5 Parametrize key selection for the list task (#3838)
* Parametrize key selectinf for list task

* Remove trailing whitespace

* Add output_keys to RPC List Parameters

* Move up changelog entry, add contributor note

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-09-02 14:58:00 +02:00
Joel Labes
e6344205bb Add colourful count of pass/fail tests in dbt debug (#3832)
* Add colourful count of pass/fail tests in dbt debug

* Remove number of checks, move error messages into shared list

* Fix flake issues

* Update CHANGELOG.md
2021-09-02 12:15:03 +02:00
Jason Gluck
9d7a6556ef configurable postgres connect timeout (#3582)
* configurable postgres connect timeout

* changelog for #3582

* test default and change connect_timeout

* Move up contributor note in changelog

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-08-31 19:45:31 +02:00
Daniel Bartley
15f4add0b8 Add target_project and target_dataset config aliases for snapshots on BigQuery (#3834)
* add bq alias for target_project and target_dataset

* Update CHANGELOG.md

add #3694 to changelog

* Update CHANGELOG.md

Be more specific about the change to bigquery synonym for schema only.

* Set integration test bigquery configs to use alias

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-08-31 16:08:20 +02:00
Anders
464becacd0 fewer adapters will need to re-implemnt basic_load_csv_rows (#3623)
* fewer adapters will need to re-implemnt basic_load_csv_rows

* hack version

* reordering per convention

* make redundant basic_load_csv_rows

* for next version

* Update core/dbt/include/global_project/macros/materializations/seed/seed.sql

Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>

* Move up changelog entry

Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-08-31 15:47:36 +02:00
sungchun12
51a76d0d63 Better dbt packages version logging to aid in upgrading outdated packages (#3759)
* start blueprinting changes

* extend registry handler for latest package version

* conditional logging for latest version

* remove todo

* add conditional logging

* Upgrades is clearer

* update if elif conditions and log msg

* remove TODO

* fix flake8 errors

* blueprint unit tests

* conditions specific to hub registry

* 1 passing test for get latest version

* DRY method calls

* move version latest to hub only

* add a new line

* remove other draft tests

* update changelog

* update log language for clarity

* pass flake8

* fix changelog

* Update test/unit/test_deps.py

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>

* update changelog

* remove hub language

* sort for latest version and include prereleases

* fix flake8

* resolves another issue

* fix prerelease string formatting

* fix broken test

* update logging to past tense

* built-in version sorting

* handle prereleases for latest version checks

* get version latest unit test based on prerelease

* update unit test for sorting functionality

* consistent test names

* fix flake8

* clean up contributors list

* simplify if else logic

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-08-31 10:31:45 +02:00
Slava Kalashnikov
052e54d43a BigQuery copy materialization enhancement (#3606)
* Change BigQuery copy materialization

Change BigQuery copy materialization macros to copy data from several sources into single target

* Change BigQuery copy materialization

Change BigQuery connections.py to copy data from several sources into single target via copy materialization

* Change BigQuery copy materialization

Test to check default value of `copy_materialization` if it is absent in config

* Change BigQuery copy materialization

Update changelog

* Update changelog

* Var renaming + test addition

* Changelog updated

* Changelog updated

* Fix test for copy table

* Update test_bigquery_adapter.py

* Update test_bigquery_adapter.py

* Update impl.py

* Update connections.py

* Update test_bigquery_adapter.py

* Update test_bigquery_adapter.py

* Update connections.py

* Align calls from mock and from adapter

* Split long code ilnes

* Create additional.sql

* Update copy_as_several_tables.sql

* Update schema.yml

* Update copy.sql

* Update connections.py

* Update test_bigquery_copy_models.py

* Add contributor
2021-08-30 16:28:35 +02:00
Kyle Wigley
9e796671dd Update workflow concurrency (#3824) 2021-08-26 17:13:54 -04:00
Kyle Wigley
a9a6254f52 Address unexpected cancelled CI workflows and stop blocking Postgres integration tests (#3813) 2021-08-26 10:37:50 -04:00
Kyle Wigley
8b3a09c7ae Run postgres integration tests when dev dependency changes (#3819) 2021-08-26 10:36:24 -04:00
Jeremy Cohen
6aa4d812d4 Rewrite generic tests to support column expressions (#3812)
* Rewrite generic tests to support column expressions, too

* Fix naming
2021-08-26 10:30:03 -04:00
Kyle Wigley
07fa719fb0 Revert "Bump freezegun from 0.3.12 to 1.1.0" (#3818)
This reverts commit 650b34ae24.
2021-08-26 10:11:56 -04:00
dependabot[bot]
650b34ae24 Bump freezegun from 0.3.12 to 1.1.0 (#3206)
Bumps [freezegun](https://github.com/spulec/freezegun) from 0.3.12 to 1.1.0.
- [Release notes](https://github.com/spulec/freezegun/releases)
- [Changelog](https://github.com/spulec/freezegun/blob/master/CHANGELOG)
- [Commits](https://github.com/spulec/freezegun/compare/0.3.12...1.1.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-25 18:18:37 -04:00
dependabot[bot]
0a935855f3 Update sqlparse requirement from <0.4,>=0.2.3 to >=0.2.3,<0.5 in /core (#3074)
Updates the requirements on [sqlparse](https://github.com/andialbrecht/sqlparse) to permit the latest version.
- [Release notes](https://github.com/andialbrecht/sqlparse/releases)
- [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG)
- [Commits](https://github.com/andialbrecht/sqlparse/compare/0.2.3...0.4.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-25 09:59:36 -04:00
dependabot[bot]
d500aae4dc Bump ubuntu from 18.04 to 20.04 (#3073)
Bumps ubuntu from 18.04 to 20.04.

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-25 09:35:24 -04:00
Kyle Wigley
370d3e746d Remove fishtown-analytics references 😢 (#3801) 2021-08-25 09:24:41 -04:00
Kyle Wigley
ab06149c81 Moving CI to GitHub actions (#3669)
* test

* test test

* try this again

* test actions in same repo

* nvm revert

* formatting

* fix sh script for building dists

* fix windows build

* add concurrency

* fix random 'Cannot track experimental parser info when active user is None' error

* fix build workflow

* test slim ci

* has changes

* set up postgres for other OS

* update descriptions

* turn off python3.9 unit tests

* add changelog

* clean up todo

* Update .github/workflows/main.yml

* create actions for common code

* temp commit to test

* cosmetic updates

* dev review feedback

* updates

* fix build checks

* rm auto formatting changes

* review feeback: update order of script for setting up postgres on macos runner

* review feedback: add reasoning for not using secrets in workflow

* review feedback: rm unnecessary changes

* more review feedback

* test pull_request_target action

* fix path to cli tool

* split up lint and unit workflows for clear resposibilites

* rm `branches-ignore` filter from pull request trigger

* testing push event

* test

* try this again

* test actions in same repo

* nvm revert

* formatting

* fix windows build

* add concurrency

* fix build workflow

* test slim ci

* has changes

* set up postgres for other OS

* update descriptions

* turn off python3.9 unit tests

* add changelog

* clean up todo

* Update .github/workflows/main.yml

* create actions for common code

* cosmetic updates

* dev review feedback

* updates

* fix build checks

* rm auto formatting changes

* review feedback: add reasoning for not using secrets in workflow

* review feedback: rm unnecessary changes

* more review feedback

* test pull_request_target action

* fix path to cli tool

* split up lint and unit workflows for clear resposibilites

* rm `branches-ignore` filter from pull request trigger

* test dynamic matrix generation

* update label logic

* finishing touches

* align naming

* pass opts to pytest

* slim down push matrix, there are a lot of jobs

* test bump num of proc

* update matrix for all event triggers

* handle case when no changes require integration tests

* dev review feedback

* clean up and add branch name for testing

* Add test results publishing as artifact (#3794)

* Test failures file

* Add testing branch

* Adding upload steps

* Adding date to name

* Adding to integration

* Always upload artifacts

* Adding adapter type

* Always publish unit test results

* Adding comments

* rm unecessary env var

* fix changelog

* update job name

* clean up python deps

Co-authored-by: leahwicz <60146280+leahwicz@users.noreply.github.com>
2021-08-24 17:12:42 -04:00
Gerda Shank
e72895c7c9 Merge pull request #3791 from dbt-labs/3210_select_equals_model
[#3210] Make --models and --select synonyms, except for 'ls'
2021-08-24 14:44:24 -04:00
Gerda Shank
fe4a67daa4 Use 'select' instead of 'models' for internal args processing and RPC 2021-08-24 13:55:37 -04:00
leahwicz
09ea989d81 Retry GitHub download failures (#3729)
* Retry GitHub download failures

* Refactor and add tests

* Fixed linting and added comment

* Fixing unit test assertRaises

Co-authored-by: Kyle Wigley <kyle@fishtownanalytics.com>

* Fixing casing

Co-authored-by: Kyle Wigley <kyle@fishtownanalytics.com>

* Changing to use partial for function calls

Co-authored-by: Kyle Wigley <kyle@fishtownanalytics.com>
2021-08-24 13:35:09 -04:00
leahwicz
7fa14b6948 Fixing changelog (#3776) 2021-08-23 13:19:58 -04:00
Gerda Shank
d4974cd35c [#3210] Make --models and --select synonyms, except for 'ls' 2021-08-23 11:40:59 -04:00
Kyle Wigley
459178811b rm git backports for previous debian release, use git package (#3785) 2021-08-22 21:20:34 -04:00
Snyk bot
b37f6a010e fix: docker/Dockerfile to reduce vulnerabilities (#3771)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-DEBIAN10-SQLITE3-537598
- https://snyk.io/vuln/SNYK-DEBIAN10-SYSTEMD-345386
- https://snyk.io/vuln/SNYK-DEBIAN10-SYSTEMD-345386
- https://snyk.io/vuln/SNYK-DEBIAN10-SYSTEMD-345391
- https://snyk.io/vuln/SNYK-DEBIAN10-SYSTEMD-345391
2021-08-19 09:26:53 -04:00
Gerda Shank
e817164d31 Merge pull request #3767 from dbt-labs/3764_analysis_descriptions
[#3764] Fix bug in analysis patch application
2021-08-18 09:05:10 -04:00
Gerda Shank
09ce43edbf [#3764] Fix bug in analysis patch application 2021-08-17 18:07:35 -04:00
sungchun12
2980cd17df Fix/bigquery job label length (#3703)
* add blueprints to resolve issue

* revert to previous version

* intentionally failing test

* add imports

* add validation in existing function

* add passing test for length validation

* add current sanitized label

* remove duplicate var

* Make logging output 2 lines

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>

* Raise RuntimeException to better handle error

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>

* update test

* fix flake8 errors

* update changelog

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-08-17 14:54:31 -04:00
Gerda Shank
8c804de643 Merge pull request #3758 from dbt-labs/3757_pp_version_mismatch
[#3757] Produce better information about partial parsing version mismatches
2021-08-17 13:23:23 -04:00
Gerda Shank
c8241b87e6 [#3757] Produce better information about partial parsing version
mismatches.
2021-08-17 12:48:20 -04:00
Gerda Shank
f204d24ed8 Merge pull request #3616 from dbt-labs/config_in_schema_files
Configs in schema files
2021-08-17 12:18:04 -04:00
Gerda Shank
d5461ccd8b [#2401] Configs in schema files 2021-08-17 11:50:08 -04:00
Gerda Shank
a20d2d93d3 Merge pull request #3750 from dbt-labs/fix_remove_tests
[#3711] Check that test unique_id exists in nodes when removing
2021-08-16 09:06:36 -04:00
Gerda Shank
57e1eec165 [#3711] Check that test unique_id exists in nodes when removing 2021-08-13 17:23:36 -04:00
Nathaniel May
d2dbe6afe4 Merge pull request #3739 from dbt-labs/perf-testing-tweak
Bump minimum performance runs to 20
2021-08-13 14:05:10 -04:00
Gerda Shank
72eb163223 Merge pull request #3733 from dbt-labs/pp_trap_errors
Trap partial parsing errors and switch to full reparse on exceptions
2021-08-13 13:54:40 -04:00
Gerda Shank
af16c74c3a [#3725] Switch to full reparse on partial parsing exceptions. Log and
report exception information.
2021-08-13 13:38:47 -04:00
Kyle Wigley
664f6584b9 add missing versions and format (#3738) 2021-08-13 13:31:38 -04:00
Nathaniel May
76fd3bdf8c minimum performance runs to 20 2021-08-13 13:20:30 -04:00
Jeremy Cohen
b633adb881 Use is_relational check for schema caching (#3716)
* Use is_relational check for schema caching

* Fix flake8

* Update changelog
2021-08-12 18:18:28 -04:00
Jeremy Cohen
b6e534cdd0 Feature: state:modified.macros (#3559)
* First cut at state:modified for macro changes

* First cut at state:modified subselectors

* Update test_graph_selector_methods

* Fix flake8

* Fix mypy

* Update 062_defer_state_test/test_modified_state

* PR feedback. Update changelog
2021-08-12 17:21:48 -04:00
Nathaniel May
1dc4adb86f Merge pull request #3732 from dbt-labs/perf-test-project-swap
Swap dummy perf testing projects for a real one
2021-08-12 10:27:20 -04:00
Nathaniel May
0a4d7c4831 Merge pull request #3731 from dbt-labs/perf-ci-quickfix
remove pr trigger for perf workflow
2021-08-12 10:25:13 -04:00
Nathaniel May
ad67e55d74 swapping dummy perf testing projects for real one 2021-08-11 14:24:26 -04:00
Nathaniel May
2fae64a488 remove pr trigger for perf workflow 2021-08-11 14:14:08 -04:00
Nathaniel May
1a984601ee Merge pull request #3602 from dbt-labs/performance-regression-testing
Add Performance Regression Testing [Rust]
2021-08-11 10:44:51 -04:00
Jeremy Cohen
454168204c Add build RPC method (#3674)
* Add build RPC method

* Add rpc test, some required flags

* Fix flake8

* PR feedback

* Update changelog [skip ci]

* Do not skip CI when rebasing
2021-08-10 10:51:43 -04:00
Drew Banin
43642956a2 Serialize Undefined values to JSON for rpc requests (#3687)
* (#3464) Serialize Undefined values to JSON for rpc requests

* Update changelog, fix typo
2021-08-09 21:26:09 -04:00
Nathaniel May
1fe53750fa add more future work 2021-08-09 12:55:42 -04:00
Nathaniel May
8609c02383 minor change 2021-08-09 12:53:53 -04:00
Nathaniel May
355b0c496e remove unnecessary newline printing 2021-08-09 12:53:07 -04:00
Nathaniel May
cd6894acf4 add to future work 2021-08-09 12:52:53 -04:00
Nathaniel May
b90b3a9c19 avoid abandoned destructors by refactoring usage of exit 2021-08-06 14:46:10 -04:00
leahwicz
e7b8488be8 Remove converter.py since not used anymore (#3699) 2021-08-05 15:27:56 -04:00
Nathaniel May
06cc0c57e8 changelog 2021-08-05 12:27:21 -04:00
Nathaniel May
87072707ed point to manifest 2021-08-05 11:40:48 -04:00
Nathaniel May
ef63319733 add comment 2021-08-05 11:39:19 -04:00
Nathaniel May
2068dd5510 fmt 2021-08-05 11:31:19 -04:00
Nathaniel May
3e1e171c66 add test for regression calculation 2021-08-05 11:31:00 -04:00
Nathaniel May
5f9ed1a83c run twice a day 2021-08-05 11:15:40 -04:00
Nathaniel May
3d9e54d970 add fmt check 2021-08-05 11:14:30 -04:00
Nathaniel May
52a0fdef6c fmt 2021-08-05 11:10:07 -04:00
Nathaniel May
d9b02fb0a0 add lots of comments 2021-08-05 11:03:13 -04:00
Nathaniel May
6c8de62b24 up stddev threshold 2021-08-05 09:48:33 -04:00
Nathaniel May
2d3d1b030a rename to dummy project 2021-08-05 09:47:34 -04:00
Nathaniel May
88acf0727b remove clone 2021-08-04 17:50:32 -04:00
Nathaniel May
02839ec779 iter instead of into_iter 2021-08-04 17:48:13 -04:00
Nathaniel May
44a8f6a3bf more reference improvements 2021-08-04 17:46:45 -04:00
Nathaniel May
751ea92576 make the happy path use references and clone in the exception paths 2021-08-04 17:33:29 -04:00
Nathaniel May
02007b3619 more reference handling 2021-08-04 17:05:23 -04:00
Nathaniel May
fe0b9e7ef5 refactor to use references better 2021-08-04 17:00:16 -04:00
Nathaniel May
4b1c6b51f9 fix spacing 2021-08-04 16:39:53 -04:00
Nathaniel May
0b4689f311 address PR feedback 2021-08-04 15:14:37 -04:00
Nathaniel May
b77eff8f6f errors to warnings in ci 2021-08-04 13:34:46 -04:00
Nathaniel May
2782a33ecf minor refactor 2021-08-04 13:31:48 -04:00
Nathaniel May
94c6cf1b3c remove some clones 2021-08-04 13:19:39 -04:00
Nathaniel May
3c8daacd3e refactor for simpler flow 2021-08-04 13:12:01 -04:00
Nathaniel May
2f9907b072 remove one more unwrap 2021-08-04 12:34:57 -04:00
Nathaniel May
287c4d2b03 revamp exception hierarchy 2021-08-04 12:15:29 -04:00
Nathaniel May
ba9d76b3f9 make final json human readable 2021-08-04 10:11:09 -04:00
Jeremy Cohen
0efaaf7daf Fix typo [skip ci] 2021-08-04 09:50:11 -04:00
Drew Banin
9ae7d68260 Merge pull request #3686 from dbt-labs/fix/cleanup-audit-integration-tests
Fix: Drop audit schema tests in tearDown for test suite
2021-08-03 19:54:36 -04:00
Nathaniel May
486afa9fcd upload results even when regressions are detected 2021-08-03 18:04:22 -04:00
Nathaniel May
1f189f5225 added small paragraph to readme 2021-08-03 17:59:53 -04:00
Nathaniel May
580b1fdd68 minor print change 2021-08-03 17:56:30 -04:00
Nathaniel May
bad0198a36 minor readme changes 2021-08-03 17:55:03 -04:00
Nathaniel May
252280b56e write out results as artifact 2021-08-03 17:34:10 -04:00
Nathaniel May
64bf9c8885 test fix 2021-08-03 17:04:13 -04:00
Nathaniel May
935c138736 type gymnastics 2021-08-03 17:03:37 -04:00
Nathaniel May
5891b59790 better variable names 2021-08-03 16:52:28 -04:00
Nathaniel May
4e020c3878 enforce branch names at calculation time 2021-08-03 16:50:08 -04:00
Nathaniel May
3004969a93 move measure io to main 2021-08-03 16:26:29 -04:00
Nathaniel May
873e9714f8 move io operations to main 2021-08-03 16:22:02 -04:00
Nathaniel May
fe24dd43d4 return all calculations not just regressions 2021-08-03 16:10:16 -04:00
Nathaniel May
ed91ded2c1 branches named dev and baseline 2021-08-03 15:04:05 -04:00
Nathaniel May
757614d57f add stddev regression 2021-08-03 14:59:21 -04:00
Nathaniel May
faff8c00b3 group by run only 2021-08-03 14:37:49 -04:00
Github Build Bot
45fe76eef4 Merge remote-tracking branch 'origin/releases/0.21.0b1' into develop 2021-08-03 18:09:56 +00:00
Nathaniel May
80244a09fe add error for groups that are not two elements large 2021-08-03 14:00:30 -04:00
Github Build Bot
ea772ae419 Release dbt v0.21.0b1 2021-08-03 17:30:32 +00:00
Drew Banin
c68fca7937 Fix: Drop audit schema tests in tearDown for test suite 2021-08-03 13:24:54 -04:00
Nathaniel May
37e86257f5 point to the downloaded artifacts correctly 2021-08-03 12:57:19 -04:00
Nathaniel May
c182c05c2f error when there are no results to process 2021-08-03 12:48:21 -04:00
Nathaniel May
b02875a12b add debug line 2021-08-03 12:35:32 -04:00
Nathaniel May
03332b2955 more gracefully exit than unwrapping 2021-08-03 12:32:56 -04:00
Nathaniel May
f1f99a2371 add name to action 2021-08-03 12:15:44 -04:00
Nathaniel May
95116dbb5b fix measure call 2021-08-03 12:12:25 -04:00
Nathaniel May
868fd64adf download correct artifact 2021-08-03 12:08:00 -04:00
Nathaniel May
2f7ab2d038 unify rust apps as subcommands 2021-08-03 12:04:45 -04:00
Jeremy Cohen
159e79ee6b Update changelog in advance of v0.21.0b1 (#3678)
* Fixup Changelog

* More updates [skip ci]
2021-08-02 20:08:22 -04:00
Nathaniel May
3d4a82cca2 add comments 2021-08-02 18:09:31 -04:00
Nathaniel May
6ba837d73d fix download artifact name 2021-08-02 18:03:04 -04:00
Nathaniel May
f4775d7673 change job name 2021-08-02 17:56:29 -04:00
Nathaniel May
429396aa02 move step dependencies around 2021-08-02 17:55:26 -04:00
Nathaniel May
8a5e9b71a5 add some output to comparitor 2021-08-02 17:50:33 -04:00
Nathaniel May
fa78102eaf add comparison to workflow 2021-08-02 17:23:30 -04:00
Nathaniel May
5466d474c5 add test for exception display 2021-08-02 17:07:41 -04:00
Nathaniel May
80951ae973 fmt 2021-08-02 16:34:47 -04:00
Nathaniel May
d5662ef34c wrap up comparsion logic 2021-08-02 16:33:43 -04:00
leahwicz
57783bb5f6 Adding issue templates for different release types (#3644)
Co-authored-by: Kyle Wigley <kyle@fishtownanalytics.com>
Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-08-02 12:50:49 -04:00
Nathaniel May
d73ee588e5 Merge pull request #3637 from dbt-labs/experimental-parser-fix
make experimental parser respect config merge behavior
2021-08-02 10:03:42 -04:00
Nathaniel May
40089d710b experimental parser respects config merge behavior 2021-08-02 09:38:30 -04:00
Jeremy Cohen
6ec61950eb Handle exception from tracker.flush() (#3661) 2021-08-02 08:25:41 -04:00
Gerda Shank
72c831a80a Merge pull request #3659 from dbt-labs/pp_internal_macro_processing
[#3636] Check for unique_ids when recursively removing macros
2021-07-30 15:34:14 -04:00
Gerda Shank
929931a26a Merge pull request #3654 from dbt-labs/change_config_call_handling
Switch from config_call list to config_call_dict dictionary
2021-07-30 14:08:30 -04:00
Gerda Shank
577e2438c1 [#3636] Check for unique_ids when recursively removing macros 2021-07-30 14:01:40 -04:00
Kyle Wigley
2679792199 Add tracking event for full re-parse reasoning (#3652)
* add tracking event for full reparse reason

* update changelog
2021-07-30 09:39:09 -04:00
Kyle Wigley
2adf982991 update links to dbt repo (#3521) 2021-07-30 08:46:58 -04:00
Gerda Shank
1fb4a7f428 Switch from config_call list to config_call_dict dictionary 2021-07-29 18:46:59 -04:00
Kyle Wigley
30e72bc5e2 Use SchemaParser render context to render test configs (#3646)
* use available context when rendering test configs

* add test

* update changelog
2021-07-29 12:59:48 -04:00
Jeremy Cohen
35645a7233 Include dbt-docs changes for 0.20.1-rc1 (#3643) 2021-07-29 09:56:04 -04:00
Gerda Shank
d583c8d737 Merge pull request #3632 from dbt-labs/pp_delete_schema_macro_patch
[#3627] Improve findability of macro_patches, schedule right macro file for processing
2021-07-28 17:49:27 -04:00
Gerda Shank
a83f00c594 [#3627] Improve findability of macro_patches, schedule right macro file
for processing
2021-07-28 17:27:42 -04:00
Nathaniel May
45bb955b55 still wip but compiles 2021-07-28 15:54:04 -04:00
Daniele Frigo
c448702c1b Use old_relation for renaming in default materializations (#3547)
* table and view materializations should rename from old_relation to manage changes from view to table and reverse

* edited changelog

* edited changelog

* Update CHANGELOG.md

Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>

Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-07-28 06:59:27 -04:00
Niall Woodward
558a6a03ac Fix PR link in changelog (#3639)
Fix a typo introduced in https://github.com/dbt-labs/dbt/pull/3624
2021-07-28 06:51:45 -04:00
Niall Woodward
52ec7907d3 dbt deps prerelease install bugs + add install-prerelease parameter to packages.yml (#3624)
* Fix dbt deps prerelease install bugs

* Add install-prerelease parameter to hub packages in packages.yml
2021-07-27 21:59:46 -04:00
Jeremy Cohen
792f39a888 Snowflake: no transactions, except for DML (#3510)
* Rm Snowflake txnal logic. Explicit for DML

* Be less clever. Update create_or_replace_view()

* Seed DML as well

* Changelog entry

* Fix unit test

* One semicolon can change the world
2021-07-27 18:13:35 -04:00
Gerda Shank
16264f58c1 Merge pull request #3621 from dbt-labs/pp_macro_link_processing_error
[#3584] Partial parsing: handle source tests when changing test macro
2021-07-27 16:59:26 -04:00
Nathaniel May
2317c0c3c8 Merge pull request #3630 from dbt-labs/nate-3568
fix awkward exception being raised by a yml file with all comments
2021-07-27 16:50:56 -04:00
Gerda Shank
3c09ab9736 [#3584] Partial parsing: handle source tests when changing test macro 2021-07-27 16:34:23 -04:00
Gerda Shank
f10dc0e1b3 Merge pull request #3618 from dbt-labs/pp_yaml_version
[#3567] Fix partial parsing error with version key if previous file is empty
2021-07-27 16:30:06 -04:00
leahwicz
634bc41d8a Secret scrubbing for env variables (#3617)
Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-07-27 16:06:10 -04:00
Gerda Shank
d7ea3648c6 [#3567] Fix partial parsing error with version key if previous file is
empty
2021-07-27 15:38:52 -04:00
Gerda Shank
e5c8e19ff2 Merge pull request #3619 from dbt-labs/model_config_iterator
[#3573] Put back config iterator for backwards compatibility
2021-07-27 15:34:51 -04:00
Kyle Wigley
4ddba7e44c --wip-- 2021-07-27 12:48:33 -04:00
Kyle Wigley
37b31d10c8 add derived traits to structs 2021-07-27 11:28:32 -04:00
Nathaniel May
93cf1f085f handle None return value from yaml loading 2021-07-27 10:59:27 -04:00
Gerda Shank
a84f824a44 [#3573] Put back config iterator for backwards compatibility 2021-07-26 17:56:35 -04:00
Kyle Wigley
9c58f3465b Fix flaky test related to tracking events (#3604)
* skip all tracking event testing

* Turn off tracking in tests that hits model parsing code path
fix other random test that fails because global tracking.current_user exists but is null

* pytest did not respect skip mark

* fix gh actions
2021-07-26 16:55:16 -04:00
Gerda Shank
0e3778132b Merge pull request #3620 from dbt-labs/pp_already_removed_node
If SQL file already scheduled for parsing, don't reprocess
2021-07-26 15:49:10 -04:00
Jeremy Cohen
72722635f2 Fix error handling in dbt build (#3608)
* RunTask -> BuildTask

* Add test, changelog entry
2021-07-25 22:15:13 -04:00
Gerda Shank
a4c7c7fc55 If SQL file already scheduled for parsing, don't reprocess 2021-07-24 15:43:54 -04:00
Nathaniel May
2bad73eead Merge pull request #3610 from dbt-labs/derp-fix
fixing typo in test
2021-07-23 13:14:55 -04:00
Kyle Wigley
c8bc25d11a add struct 2021-07-22 17:05:08 -04:00
Kyle Wigley
4c06689ff5 create a tuple of measurements to group results by 2021-07-22 16:54:57 -04:00
Kyle Wigley
a45c9d0192 add new arg for runner for branch name 2021-07-22 16:24:46 -04:00
Kyle Wigley
34e2c4f90b fix results output dir 2021-07-22 16:16:21 -04:00
Kyle Wigley
c0e2023c81 update profiles dir path 2021-07-22 15:52:28 -04:00
Nathaniel May
108b55bdc3 add regression type 2021-07-22 13:50:17 -04:00
Nathaniel May
a29367b7fe read files and parse json 2021-07-22 12:30:35 -04:00
Nathaniel May
1d7e8349ed draft of comparison 2021-07-22 12:01:30 -04:00
Nathaniel May
67c194dcd1 fixing typo in test 2021-07-22 09:53:26 -04:00
Nathaniel May
75d3d87d64 fix warmup syntax 2021-07-21 18:00:07 -04:00
Nathaniel May
4ff3f6d4e8 moved config dir out of projects dir 2021-07-21 17:35:54 -04:00
Nathaniel May
d0773f3346 warmup fs caches 2021-07-21 17:34:22 -04:00
Nathaniel May
ee58d27d94 use a profiles.yml file 2021-07-21 17:28:28 -04:00
Nathaniel May
9e3da391a7 draft of regression testing workflow 2021-07-21 17:01:01 -04:00
matt-winkler
bd7010678a Feature: on_schema_change for incremental models (#3387)
* detect and act on schema changes

* update incremental helpers code

* update changelog

* fix error in diff_columns from testing

* abstract code a bit further

* address matching names vs. data types

* Update CHANGELOG.md

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>

* updates from Jeremy's feedback

* multi-column add / remove with full_refresh

* simple changes from JC's feedback

* updated for snowflake

* reorganize postgres code

* reorganize approach

* updated full refresh trigger logic

* fixed unintentional wipe behavior

* catch final else condition

* remove WHERE string replace

* touch ups

* port core to snowflake

* added bigquery code

* updated impacted unit tests

* updates from linting tests

* updates from linting again

* snowflake updates from further testing

* fix logging

* clean up incremental logic

* updated for bigquery

* update postgres with new strategy

* update nodeconfig

* starting integration tests

* integration test for ignore case

* add test for append_new_columns

* add integration test for sync

* remove extra tests

* add unique key and snowflake test

* move incremental integration test dir

* update integration tests

* update integration tests

* Suggestions for #3387 (#3558)

* PR feedback: rationalize macros + logging, fix + expand tests

* Rm alter_column_types, always true for sync_all_columns

* update logging and integration test on sync

* update integration tests

* test fix SF integration tests

Co-authored-by: Matt Winkler <matt.winkler@fishtownanalytics.com>

* rename integration test folder

* Update core/dbt/include/global_project/macros/materializations/incremental/incremental.sql

Accept Jeremy's suggested change

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>

* Update changelog [skip ci]

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-07-21 15:49:19 -04:00
leahwicz
9f716b31b3 Moving unit tests into separate workflow (#3588)
* Moving unit tests into separate workflow

* Fixing CircleCI error
2021-07-21 12:35:04 -04:00
Kyle Wigley
3dd486d8fa Source freshness task node selection and cli command parity (#3554)
* cli: add selection args for source freshness command

* rename command to `source freshness` and maintain alias to old command

* update and add tests for source freshness command and node selection

* update changelog, add comments

* fix formatting

* update changelog
2021-07-21 10:31:40 -04:00
Jeremy Cohen
33217891ca Refactor relationships test to support where config (#3583)
* Rewrite relationships with CTEs

* Update changelog PR num [skip ci]
2021-07-20 19:28:09 -04:00
dependabot[bot]
1d37c4e555 Update snowflake-connector-python[secure-local-storage] requirement (#3594)
Updates the requirements on [snowflake-connector-python[secure-local-storage]](https://github.com/snowflakedb/snowflake-connector-python) to permit the latest version.
- [Release notes](https://github.com/snowflakedb/snowflake-connector-python/releases)
- [Commits](https://github.com/snowflakedb/snowflake-connector-python/commits)

---
updated-dependencies:
- dependency-name: snowflake-connector-python[secure-local-storage]
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-20 14:01:35 -04:00
Nathaniel May
9f62ec2153 add rust performance runner 2021-07-20 09:42:12 -04:00
Jeremy Cohen
372eca76b8 Bump werkzeug lower bound, werkzeug-refresh-token script (#3590)
* Update werkzeug, refresh-token script

* Add changelog note
2021-07-20 08:45:56 -04:00
Ian Knox
e3cb050bbc Merge pull request #3490 from dbt-labs/feature/dbt-build
dbt `build`
2021-07-19 19:09:50 -05:00
Jeremy Cohen
0ae93c7f54 Fix store_failures modifier for unique, not_null (#3577)
* Fix store_failures modifier for unique, not_null

* Add test, changelog
2021-07-16 14:50:06 -04:00
leahwicz
1f6386d760 Adding missing v0.20.0 files back to develop (#3566) 2021-07-15 16:46:22 -04:00
Ian Knox
66eb3964e2 PR feedback, Changelog 2021-07-14 13:23:46 -05:00
Nathaniel May
f460d275ba add experimental parser tracking (#3553) 2021-07-09 17:13:34 -04:00
Jeremy Cohen
fb91bad800 Update create_adapter_plugins.py (#3509)
* Update create_adapter_plugins.py

* Bump, changelog [skip ci]
2021-07-09 14:53:01 -04:00
Jeremy Cohen
eaec22ae53 Reconcile changelogs bw 0.20.latest + develop (#3552) 2021-07-09 13:39:42 -04:00
Jeremy Cohen
b7c1768cca Update changelog (#3551) 2021-07-09 13:35:16 -04:00
Nathaniel May
387b26a202 Merge pull request #3549 from dbt-labs/test-tweak
Tweak function to return a value instead of asserting
2021-07-09 12:20:22 -04:00
Ian Knox
8a1e6438f1 Revert downstream blocking on test failure logic due to test issues 😧 2021-07-09 10:52:12 -05:00
Ian Knox
aaac5ff2e6 New node discovery logic for internal work queue 2021-07-09 10:52:12 -05:00
Ian Knox
4dc29630b5 Race condition fix + downstream test blocking 2021-07-09 10:52:12 -05:00
Ian Knox
f716631439 Grouped Topologic sorting logic 2021-07-09 10:52:12 -05:00
Ian Knox
648a780850 First pass dbt build, cli only 2021-07-09 10:51:53 -05:00
Jeremy Cohen
de0919ff88 Include dbt-docs changes for 0.20.0 final (#3544) 2021-07-09 11:33:13 -04:00
Nathaniel May
8b1ea5fb6c return value from test function 2021-07-08 18:14:38 -04:00
Jeremy Cohen
85627aafcd Speed up Snowflake column comments, while still avoiding errors (#3543)
* Have our cake and eat it quickly, too

* Update changelog
2021-07-07 18:18:26 -04:00
Jeremy Cohen
49065158f5 Add gitignore, ignore pycache (#3536) 2021-07-06 11:51:17 -04:00
Gerda Shank
bdb3049218 Merge pull request #3522 from dbt-labs/pp_fix_simul_model_patch_deletion
Partial parsing: check if a node has already been deleted [#3516]
2021-07-06 11:46:02 -04:00
jmriego
e10d1b0f86 '+' config prefix handling whitespace (#3526)
* '+' config prefix handling whitespace

* rerun ci
2021-07-02 12:25:18 -04:00
Drew Banin
83b98c8ebf Merge pull request #3499 from dbt-labs/fix/agate-undesirable-casting
Prevent Agate from coercing values in query result sets
2021-07-01 10:53:28 -04:00
Jeremy Cohen
b9d5123aa3 Update changelog for 0.20.0rc2 2021-06-30 16:24:25 -04:00
Gerda Shank
c09300bfd2 Partial parsing: check if a node has already been deleted [#3516] 2021-06-30 11:35:13 -04:00
leahwicz
fc490cee7b Adding link to plugin release instructions 2021-06-30 09:51:53 -04:00
Jeremy Cohen
3baa3d7fe8 Update badge links 2021-06-30 08:46:33 -04:00
Jeremy Cohen
764c7c0fdc Update logo, badges (#3518)
* Fix logo, badges

* Update commit hash

* A few more edits

* Point Circle badge to develop [skip ci]

* Final fixups [skip ci]
2021-06-30 08:43:05 -04:00
Drew Banin
c97ebbbf35 Update License.md (#3517) 2021-06-29 22:43:50 -04:00
Drew Banin
85fe32bd08 Move to 0.21 changelog section 2021-06-29 20:21:31 -04:00
Drew Banin
eba3fd2255 Merge branch 'develop' of github.com:fishtown-analytics/dbt into fix/agate-undesirable-casting 2021-06-29 20:20:18 -04:00
Jeremy Cohen
e2f2c07873 Include dbt-docs changes for 0.20.0rc2 (#3511) 2021-06-29 18:44:42 -04:00
Nathaniel May
70850cd362 Merge pull request #3497 from fishtown-analytics/experimental-parser-rust
Experimental Parser: Swap python extractor for rust dependency
2021-06-29 16:21:26 -04:00
Gerda Shank
16992e6391 Merge pull request #3505 from fishtown-analytics/pp_testing
Expand partial parsing tests; fix macro partial parsing [#3449]
2021-06-29 15:45:33 -04:00
Gerda Shank
fd0d95140e Expand partial parsing tests; fix macro partial parsing [#3449] 2021-06-29 11:53:38 -04:00
Nathaniel May
ac65fcd557 update experimental parser to use rust dependency 2021-06-28 17:30:07 -04:00
Kyle Wigley
4d246567b9 Update project load tracking to include experimental parser info (#3495)
* Fix docs generation for cross-db sources in REDSHIFT RA3 node (#3408)

* Fix docs generating for cross-db sources

* Code reorganization

* Code adjustments according to flake8

* Error message adjusted to be more precise

* CHANGELOG update

* add static analysis info to parsing data

* update changelog

* don't use `meta`! need better separation between dbt internal objects and external facing data. hacked an internal field on the manifest to save off this parsing info for the time being

* fix partial parsing case

Co-authored-by: kostek-pl <67253952+kostek-pl@users.noreply.github.com>
2021-06-28 10:09:50 -04:00
Drew Banin
1ad1c834f3 (#2984) Prevent Agate from coercing values in query result sets 2021-06-26 13:24:30 -04:00
Jeremy Cohen
41610b822c Touch up init, fix missing adapter errors (#3483)
* Some love to init

* Update changelog
2021-06-24 15:51:36 -04:00
leahwicz
c794600242 Move starter project into dbt repo (#3474)
Addresses issue #3005
2021-06-22 11:03:01 -04:00
Jessica Laughlin
9d414f6ec3 add optional SSL parameters to Postgres connector (#3473)
Allows users to optionally set values for sslcert, sslkey, and
sslrootcert in their Postgres profiles.

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-06-21 17:52:59 -04:00
Ted Conbeer
552e831306 Feature/faster materializations with fewer transactions (#3468)
* drop if exists in view and table materializations

* Add integration test

* Add changelog entry

* PR Review 1
2021-06-21 14:58:32 -04:00
leahwicz
c712c96a0b Commenting our flaky integration test (#3477)
Test continually fails on time out so commenting out until we can fix it
2021-06-21 10:51:36 -04:00
Gerda Shank
eb46bfc3d6 Merge pull request #3460 from fishtown-analytics/minimal_pp_validation
Add minimal validation of schema file yaml prior to partial parsing
2021-06-18 15:08:59 -04:00
dependabot[bot]
f52537b606 Update typing-extensions requirement in /core (#3310)
Updates the requirements on [typing-extensions](https://github.com/python/typing) to permit the latest version.
- [Release notes](https://github.com/python/typing/releases)
- [Commits](https://github.com/python/typing/compare/3.7.4...3.10.0.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: leahwicz <60146280+leahwicz@users.noreply.github.com>
2021-06-18 13:05:55 -04:00
dependabot[bot]
762419d2fe Update werkzeug requirement from <2.0,>=0.15 to >=0.15,<3.0 in /core (#3390)
Updates the requirements on [werkzeug](https://github.com/pallets/werkzeug) to permit the latest version.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/0.15.0...2.0.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-18 10:16:28 -04:00
dependabot[bot]
4feb7cb15b Update idna requirement from <3,>=2.5 to >=2.5,<4 in /core (#3429)
Updates the requirements on [idna](https://github.com/kjd/idna) to permit the latest version.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)
- [Commits](https://github.com/kjd/idna/compare/v2.5...v3.2)

---
updated-dependencies:
- dependency-name: idna
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-18 09:52:38 -04:00
Gerda Shank
eb47b85148 Add minimal validation of schema file yaml prior to partial parsing
[#3246]
2021-06-17 13:25:04 -04:00
Anders
9faa019a07 dispatch logic of new test materialization (#3461)
* dispatch logic of new test materialization

allow custom adapters to override the core test select statement functionality

* rename macro

* raconte moi une histoire

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-06-17 12:09:34 -04:00
Jeremy Cohen
9589dc91fa Fix quoting for stringy test configs (#3459)
* Fix quoting for stringy test configs

* Update changelog
2021-06-16 14:26:07 -04:00
Gerda Shank
14507a283e Merge pull request #3454 from fishtown-analytics/adapter_dispatch_infinite_recursion
Fix macro depends_on recursion error when macros call themselves (dbt…utils.datediff)
2021-06-11 11:22:05 -04:00
Gerda Shank
af0fe120ec Fix macro depends_on recursion error when macros call themselves (dbt_utils.datediff) 2021-06-11 10:28:16 -04:00
Gerda Shank
16501ec1c6 Merge pull request #3445 from fishtown-analytics/fix_serialization
create _lock when deserializing manifest, plus cleanup file serialization
2021-06-11 09:30:10 -04:00
leahwicz
bf867f6aff Add minor version release template (#3442)
Add minor version release template
2021-06-09 09:31:21 -04:00
kostek-pl
eb4ad4444f Fix docs generation for cross-db sources in REDSHIFT RA3 node (#3408)
* Fix docs generating for cross-db sources

* Code reorganization

* Code adjustments according to flake8

* Error message adjusted to be more precise

* CHANGELOG update
2021-06-09 09:08:52 -04:00
Gerda Shank
8fdba17ac6 create _lock when deserializing manifest, plus cleanup file
serialization
2021-06-08 16:08:12 -04:00
Github Build Bot
abe8e83945 Merge remote-tracking branch 'origin/releases/0.20.0rc1' into develop 2021-06-04 19:02:42 +00:00
Github Build Bot
02cbae1f9f Release dbt v0.20.0rc1 2021-06-04 18:31:00 +00:00
Gerda Shank
65908b395f Merge pull request #3432 from fishtown-analytics/docs_references
Save doc file node references and use in partial parsing
2021-06-04 13:50:52 -04:00
Gerda Shank
4971395d5d Save doc file node references and use in partial parsing 2021-06-04 13:26:19 -04:00
Kyle Wigley
eeec2038aa bump run results and manifest artifact schemas versions (#3421)
* bump run results and manifest artifact schemas versions

* update changelog
2021-06-04 12:32:24 -04:00
Kyle Wigley
4fac086556 Add deprecation warning for providing packages to adapter.dispatch (#3420)
* add deprecation warning

* update changelog

* update deprecation message

* fix flake8
2021-06-04 10:11:31 -04:00
Jeremy Cohen
8818061d59 One more dbt-docs change for v0.20.0rc1 (#3430)
* Include dbt-docs changes for 0.20.0rc1

* One more dbt-docs change for 0.20.0rc1
2021-06-04 08:54:45 -04:00
leahwicz
b195778eb9 Updating triggers to not have duplicate runs (#3417)
Updating triggers to not have duplicate runs
2021-06-04 08:38:46 -04:00
Jeremy Cohen
de1763618a Avoid repeating nesting in test query (#3427) 2021-06-03 16:57:17 -04:00
Jeremy Cohen
7485066ed4 Include dbt-docs changes for 0.20.0rc1 (#3424) 2021-06-03 16:43:16 -04:00
Jeremy Cohen
15ce956380 Call out breaking changes in changelog [skip ci] (#3418) 2021-06-03 13:50:34 -04:00
Gerda Shank
e5c63884e2 Merge pull request #3364 from fishtown-analytics/move_partial_parsing
Move partial parsing to end of parsing. Switch to using msgpack for saved manifest.
2021-06-02 16:02:10 -04:00
Kyle Wigley
9fef62d83e update test subquery alias (#3414)
* update subquery alias

* update changelog
2021-06-02 15:44:17 -04:00
Gerda Shank
7563b997c2 Move partial parsing to end of parsing and implement new partial parsing
method. Switch to using msgpack for saved manifest.
2021-06-02 15:32:22 -04:00
leahwicz
291ff3600b Creating test workflow in Actions (#3396)
Moving the Windows tests to Actions and adding Mac tests as well
2021-06-02 11:54:37 -04:00
Kyle Wigley
2c405304ee New test configs (#3392)
* New test configs: where, limit, warn_if, error_if

* update test task and tests

* fix flake8

* Update core/dbt/parser/schemas.py

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>

* Update core/dbt/parser/schema_test_builders.py

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>

* respond to some feedback

* add failures field

* add failures to results

* more feedback

* add some test coverage

* dev review feedback

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-06-02 10:28:55 -04:00
Jeremy Cohen
1e5a7878e5 Hard code dispatch namespaces for fivetran_utils? (#3403)
* Hard code for fivetran_utils

* Add changelog entry [skip cii]
2021-06-01 10:58:18 -04:00
Stephen Bailey
d89e1d7f85 Add "tags" and "meta" properties to exposure schema (#3405)
* Add meta and tags to parsed and unparsed

* Fix tests

* Add meta and tags to unparsed schema

* Update Changelog

* Remove "optional" specifier and add default values

* Fix exposure schemas for PG Integration tests
2021-06-01 08:19:10 -04:00
Jeremy Cohen
98c015b775 Fix statically extracting macro calls for macro.depends_on.macros to be (#3363)
used in parsing schema tests by looking at the arguments to
adapter.dispatch. Includes providing an alternative way of specifying
macro search order in project config.
Collaboratively developed with Jeremy Cohen.

Co-authored-by: Gerda Shank <gerda@fishtownanalytics.com>
2021-05-27 17:13:47 -04:00
Ian Knox
a56502688f Merge pull request #3384 from fishtown-analytics/feature/ls_in_RPC
Add `ls` to RPC server
2021-05-27 14:42:49 -05:00
Jeremy Cohen
c0d757ab19 dbt test --store-failures (#3316) 2021-05-27 15:21:29 -04:00
Ian Knox
e68fd6eb7f PR Feedback 2021-05-27 12:25:18 -05:00
Ian Knox
90edc38859 fix ls test 2021-05-26 11:59:34 -05:00
matt-winkler
0f018ea5dd Bugfix/issue 3350 snowflake non json response (#3365)
* attempt at solving with while loop

* added comment on loop

* update changelog

* modified per drew's suggestions

* updates after linting
2021-05-26 12:18:16 -04:00
Ian Knox
1be6254363 updated changelog 2021-05-26 10:39:23 -05:00
Ian Knox
760af71ed2 Merge branch 'feature/ls_in_RPC' of github.com:fishtown-analytics/dbt into feature/ls_in_RPC 2021-05-26 10:21:30 -05:00
Ian Knox
82f5e9f5b2 added additional fields to list response (unique_id, original_file_path) 2021-05-26 10:21:12 -05:00
Ian Knox
988c187db3 Merge branch 'develop' into feature/ls_in_RPC 2021-05-26 09:40:17 -05:00
Ian Knox
b23129982c update default threading settings for all tasks 2021-05-26 09:37:49 -05:00
Daniel Mateus Pires
4d5d0e2150 🔨 Add ssh-client + update git using debian backports in Docker image (#3338)
* 🔨 Add ssh-client and update git version using debian backports in Docker image

* ✏️ Update CHANGELOG

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-05-24 18:39:32 -04:00
Jon Natkins
c0c487bf77 Running a snapshot with missing required configurations results in un... (#3385)
* Running a snapshot with missing required configurations results in uncaught Python exception (#3381)

* Running a snapshot with missing required configurations results in uncaught Python exception

* Add fix details to CHANGELOG

* Update CHANGELOG.md

* Update invalid snapshot test with new/improved error message

* Improve changelog message and contributors addition
2021-05-24 15:48:08 -04:00
Nathaniel May
835d805079 Merge pull request #3374 from fishtown-analytics/experiment/dbt_jinja
Add experimental parser behind flag
2021-05-21 18:25:08 -04:00
Nathaniel May
c2a767184c add changelog entry 2021-05-21 17:02:40 -04:00
Nathaniel May
1e7c8802eb add --use-experimental-parser flag, depend on tree-sitter-jinja2, add unit tests 2021-05-21 17:01:55 -04:00
Nathaniel May
a76ec42586 static parsing skateboard 2021-05-21 17:01:55 -04:00
PJGaetan
7418f36932 Allow to use a costum field as check-cols updated_at (#3376)
* Allow to use a costum field as check-cols updated_at

* Clarify changlog /w jtcohen6
2021-05-21 16:15:56 -04:00
Ian Knox
f9ef5e7e8e changelog 2021-05-21 09:27:54 -05:00
Ian Knox
dbfa351395 tests and fixes 2021-05-21 09:22:18 -05:00
Jeremy Cohen
e775f2b38e Use shutil.which to find executable path (#3299)
* (explicitly) find the executable before running run_cmd

#3035

* fix undefined var

* use Executable to say exe not found and use full pth to exe

* changelog for #3035

* Nest shutil.which for better error msg

Co-authored-by: Majid alDosari <majidaldosari-github@yahoo.com>
Co-authored-by: Kyle Wigley <kyle@fishtownanalytics.com>
2021-05-20 17:30:56 -04:00
dependabot[bot]
6f27454be4 Bump jinja2 from 2.11.2 to 2.11.3 in /core (#3077)
Bumps [jinja2](https://github.com/pallets/jinja) from 2.11.2 to 2.11.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/2.11.2...2.11.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-20 10:54:07 -04:00
Ian Knox
201723d506 ls in RPC works 2021-05-20 08:27:16 -05:00
Josh Devlin
17555faaca Add a better error for undefined macros (#3343)
* Add a better error for undefined macros

* Add check/error when installed packages < specified packages

* fix integration tests

* Fix issue with null packages

* Don't call _get_project_directories() twice

Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>

* Fix some integration and unit tests

* Make mypy happy

Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>

* Fix docs and rpc integration tests

* Fix (almost) all the rpc tests

Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-05-19 14:08:30 -04:00
Kyle Wigley
36e0ab9f42 Merge pull request #3339 from fishtown-analytics/fix/packaging-dep
add 'packaging' package to dbt-core
2021-05-19 11:24:30 -04:00
Kyle Wigley
6017bd6cba Merge branch 'develop' into fix/packaging-dep 2021-05-19 10:26:06 -04:00
Claire Carroll
30fed8d421 Refactor schema tests (#3367) 2021-05-18 18:15:13 -04:00
Kyle Wigley
8ac5cdd2e1 Merge pull request #3351 from fishtown-analytics/fix/bigquery-debug-task
Fix debug task for BigQuery connections
2021-05-14 14:22:23 -04:00
Kyle Wigley
114ac0793a update changelog 2021-05-14 11:02:04 -04:00
Kyle Wigley
d0b750461a fix debug task for bigquery connections 2021-05-14 11:01:23 -04:00
Jeremy Cohen
9693170eb9 Cleanup v0.20.0 changelog [skip ci] (#3323) 2021-05-13 18:44:41 -04:00
Jeremy Cohen
bbab6c2361 Separate compiled_path in manifest + printer (#3327) 2021-05-13 18:29:17 -04:00
Gerda Shank
cfe3636c78 Merge pull request #3342 from fishtown-analytics/split_out_schema_parsing
Do schema file parsing after parsing all other files
2021-05-13 16:11:05 -04:00
Gerda Shank
aadf3c702e Do schema file parsing after parsing all other files 2021-05-13 15:26:03 -04:00
Kyle Wigley
1eac726a07 fix expected redshift stats (I think the possible values for the svv_table_info.encoded column changed) 2021-05-13 13:49:02 -04:00
Gerda Shank
85e2c89794 Merge pull request #3345 from fishtown-analytics/package_name_macros
Handle macros with package names in schema test rendering
2021-05-12 21:53:46 -04:00
Eli Kastelein
fffcd3b404 Check if a snowflake column exists before altering its comment (#3149)
* Check if column exists when altering column comments in snowflake

* Add new test class for persist docs models with missing columns

* Parallel run all integration tests after unit (#3328)

* don't clobber default args

* update changelog

* Update changelog for PR #3149

* Pull in upstream changes

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
Co-authored-by: Kyle Wigley <kyle@fishtownanalytics.com>
2021-05-12 21:33:01 -04:00
peiwangdb
fbfef4b1a3 fix integration test failure due to timezone (#3344)
* fix integration test failure due to timezone

* update changelog
2021-05-12 21:28:19 -04:00
Gerda Shank
526a6c0d0c Merge branch 'develop' into package_name_macros 2021-05-12 21:21:45 -04:00
Ian Knox
1f33b6a74a Merge pull request #3335 from fishtown-analytics/feature/schema_tests_are_more_unique
Feature/schema tests are more unique
2021-05-12 19:18:50 -05:00
Ian Knox
95fc6d43e7 Additional PR feedback 2021-05-12 13:29:24 -05:00
Kyle Wigley
d8c261ffcf Merge pull request #3340 from fishtown-analytics/fix/default-test-args
Stop clobbering default args for test definitions
2021-05-12 10:25:53 -04:00
Kyle Wigley
66ea0a9e0f update changelog 2021-05-12 09:54:20 -04:00
Gerda Shank
435b542e7b Add macros with project name to schema test context, and recursively get
macros
2021-05-12 09:51:44 -04:00
Ian Knox
10cd06f515 PR feedback 2021-05-12 08:38:50 -05:00
Jeremy Cohen
9da1868c3b Parallel run all integration tests after unit (#3328) 2021-05-11 13:23:38 -04:00
Kyle Wigley
2649fac4a4 don't clobber default args 2021-05-11 10:45:16 -04:00
Kyle Wigley
6e05226e3b update changelog 2021-05-11 07:18:15 -04:00
Kyle Wigley
c1c3397f66 add 'packaging' package to dbt-core 2021-05-11 07:07:12 -04:00
Ian Knox
2065db2383 Testing changes to support hashes 2021-05-10 15:49:50 -05:00
Ian Knox
08fb868b63 Adds hash to generic test unique_ids 2021-05-10 15:45:07 -05:00
Aram Panasenco
8d39ef16b6 Now generating a run_results.json even when no nodes are selected (#3315)
* Now generating a run_results.json even when no nodes are selected.

* Typo in changelog

* Modified changelog in accordance with @jtcohen6's suggestions. Also fixed @TeddyCr's entry.
2021-05-06 08:07:54 -04:00
Teddy
66c5082aa7 Feature/3117 dbt deps timeout (#3275)
* added logic to timeout request after 60 seconds + unit test

* fixed typo in comment

* Update test URL for azure CI failure

* Update changelog + change endpoint for timeout test + added retry logic to the timeout exception

* updated exception catching to a generic one + adjusted tests based on error catching + renamed test file accordingly

* updated comment in test_registry_get_request_exception.py to reflect new approach

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-05-05 23:40:21 -04:00
Kyle Wigley
26fb58bd1b Merge pull request #3318 from fishtown-analytics/fix/ephemeral-compilation
[moving fix to current release] Fix ephemeral model compilation
2021-05-04 14:54:19 -04:00
Kyle Wigley
fed8826043 update changelog 2021-05-04 10:02:56 -04:00
Kyle Wigley
9af78a3249 fix tests after merge 2021-05-04 08:57:31 -04:00
Kyle Wigley
bf1ad6cd17 Merge pull request #3139 from fishtown-analytics/fix/ephemeral-compile-sql
Fix compiled sql for ephemeral models
2021-05-04 08:39:39 -04:00
Github Build Bot
15e995f2f5 Merge remote-tracking branch 'origin/releases/0.20.0b1' into develop 2021-05-03 11:33:09 +00:00
Github Build Bot
b3e73b0de8 Release dbt v0.20.0b1 2021-05-03 11:08:53 +00:00
Jeremy Cohen
dd2633dfcb Include parent adapters in dispatch (#3296)
* Add test, expect fail

* Include parent adapters in dispatch

* Use adapter type, not credentials type

* Adjust adapter_macro deprecation test

* fix test/unit/test_context.py to use postgres profile

* Add changelog note

* Redshift default column encoding now AUTO

Co-authored-by: Gerda Shank <gerda@fishtownanalytics.com>
2021-05-02 18:01:57 -04:00
Karthik Ramanathan
29f0278451 prevent locks in incremental full refresh (#2998) 2021-05-02 17:19:25 -04:00
Angel Montana
f0f98be692 Support dots in model names: Make FQN model selectors work with them (#3247)
* Support dots in model names. They are useful as namespace separators

* Update CHANGELOG.md

* Update contributor list

* Extend integration test to support models whose name contains dots

* Cleanup fqn selection login

* Explain condition check

* Support dots in model names: integration tests

* Make linter happy: remove trailing whitespaces

* Support dots in model names: integration test for seeds

* revert 66c26facbd. Integration tests to support dots in model names implemented in 007_graph_selection_tests

* Test model with dots only in postgres. It's meant to work in combination with custom macros for other databases

* Support dot as namespace separators: integration test

* Support dots in test names: integration test

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-04-28 17:32:14 -04:00
Kyle Wigley
5956a64b01 Merge pull request #3305 from fishtown-analytics/fix/bigquery-no-project
add unit test and move default logic to mashumaro hook
2021-04-28 14:52:53 -04:00
Daniel Mateus Pires
5fb36e3e2a Issue 275: Support dbt package dependencies in Git subdirectories (#3267)
* 🔨 Extend git package contract and signatures to pass `subdirectory`

* Add sparse checkout logic

*  Add test

* 🧹 Lint

* ✏️ Update CHANGELOG

* 🐛 Make os.path.join safe

* Use a test-container with an updated `git` version

* 🔨 Fix integration tests

* 📖 Update CHANGELOG contributors to include this PR

* 🧪 Parameterize the test

* Use new test-container published by @kwigley (contains more recent version of git)

* Use repositories managed by fishtown

* 🧘‍♂️ Merge the CHANGELOG

* 🤦‍♂️ Remove repetition of my contribution on the CHANGELOG

Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-04-28 09:40:05 -04:00
Cor
9d295a1d91 Add TestCLIInvocationWithProfilesAndProjectDir (#3176)
* Add TestCLIInvocationWithProfilesAndProjectDir

* Add context manager for changing working directory

* Add fixture for changing working directory to temporary directory

* Add missing import

* Move custom profile into function

* Add function to create profiles directory

* Remove path and union

* Make temporary_working_directory a context manager

* Fix some issues, wrong naming and invocation

* Update test with profiles and project dir

* Use a fixture for the sub command

* Run test for debug and run sub command

* Resolve inheritance

* Remove profiles from project dir

* Use pytest mark directly instead of use_profile

* Use parameterize on class

* Remove parameterize

* Create separate test for each subcommand

* Set profiles_dir to false

* Add run test

* Use abspath

* Add entry to change log

* Add suggested changes

Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>

* Remove test_postgres_run_with_profiles_separate_from_project_dir

* Add JCZuurmond to change log

* Add test_postgres_run_with_profiles_separate_from_project_dir

* Remove HEAD

* Fix wrong merge

* Add deps before test

* Add run to test which runs test command

* Sort tests

* Force rerun

* Force rerun

* Force rerun

Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-04-28 08:20:02 -04:00
Jeremy Cohen
39f350fe89 Be less greedy in test selection expansion (#3235)
* Expand test selection iff all first-order parents are selected

* Renumber new integration test

* PR feedback

* Fix flake
2021-04-27 15:52:16 -04:00
Kyle Wigley
8c55e744b8 Merge pull request #3286 from fishtown-analytics/feature/schema-test-materialization
use test materialization for schema/generic tests
2021-04-27 08:05:42 -04:00
Jeremy Cohen
a260d4e25b Merge pull request #3106 from arzavj/postgres_create_indexes
Postgres: ability to create indexes
2021-04-27 08:02:26 -04:00
Jeremy Cohen
509797588f Merge branch 'develop' into postgres_create_indexes 2021-04-27 07:28:38 -04:00
Kyle Wigley
2eed20f1f3 update changelog 2021-04-23 09:51:56 -04:00
Kyle Wigley
1d7b4c0db2 update integration tests, fix tests for bigquery 2021-04-22 16:24:13 -04:00
Kyle Wigley
ac8cd788cb use test materialization for schema/generic tests, update integration tests 2021-04-22 11:26:13 -04:00
Gerda Shank
33dc970859 Merge pull request #3272 from fishtown-analytics/test_context_regression
Add necessary macros to schema test context namespace
2021-04-20 12:36:40 -04:00
Kyle Wigley
f73202734c Merge pull request #3261 from fishtown-analytics/feature/test-jinja-block
Add `test` Jinja tag
2021-04-19 09:39:53 -04:00
Jeremy Cohen
32bacdab4b Merge pull request #3270 from dmateusp/dmateusp/3268/dbt_deps_support_commit_hashes
Issue 3268: Support commit hashes in dbt deps
2021-04-18 18:17:27 -04:00
Daniel Mateus Pires
6113c3b533 📖 Add myself to contribs 2021-04-18 21:33:01 +01:00
Gerda Shank
1c634af489 Add necessary macros to schema test context namespace [#3229] [#3240] 2021-04-16 13:21:18 -04:00
Daniel Mateus Pires
428cdea2dc ✏️ Update CHANGELOG 2021-04-16 10:58:04 +01:00
Daniel Mateus Pires
f14b55f839 Add test 2021-04-16 10:54:35 +01:00
Daniel Mateus Pires
5934d263b8 Support git commit as revision 2021-04-16 10:21:49 +01:00
Kyle Wigley
3860d919e6 use ternary and f-strings 2021-04-15 14:49:07 -04:00
Kyle Wigley
fd0b9434ae update changelog 2021-04-15 14:49:07 -04:00
Kyle Wigley
efb30d0262 first pass at adding test jinja block 2021-04-15 14:48:12 -04:00
Jeremy Cohen
cee0bfbfa2 Merge pull request #3257 from fishtown-analytics/feature/test-config-parity
Feature: test config parity
2021-04-15 14:15:19 -04:00
Jeremy Cohen
dc684d31d3 Add changelog entry 2021-04-15 14:04:34 -04:00
Gerda Shank
bfdf7f01b5 Merge pull request #3248 from fishtown-analytics/load_all_files
Preload all project files at start of parsing [#3244]
2021-04-14 13:33:32 -04:00
Gerda Shank
2cc0579b6e Preload all project files at start of parsing [#3244] 2021-04-14 09:57:26 -04:00
Jeremy Cohen
bfc472dc0f Cleanup + integration tests 2021-04-13 20:04:53 -04:00
Jeremy Cohen
ea4e3680ab Configure tests from dbt_project.yml 2021-04-13 20:04:42 -04:00
Jeremy Cohen
f02139956d Add support for disabling schema tests 2021-04-13 20:04:42 -04:00
Arzav Jain
cacbd1c212 Updated Changelog.md 2021-04-12 09:38:42 -07:00
Arzav Jain
3f78bb7819 more tests 2021-04-12 09:37:54 -07:00
Arzav Jain
aa65b01fe3 basic tests for table materialization 2021-04-12 09:37:54 -07:00
Arzav Jain
4f0968d678 fix style to get unit tests to pass 2021-04-12 09:37:54 -07:00
Arzav Jain
118973cf79 respond to pr comments 2021-04-12 09:37:54 -07:00
Arzav Jain
df7cc0521f remove no longer used truncate_string macro 2021-04-12 09:37:54 -07:00
Arzav Jain
40c02d2cc9 Respond to pr comments; approach inspired by persist_docs() 2021-04-12 09:37:54 -07:00
Arzav Jain
be70b1a0c1 first pass 2021-04-12 09:37:44 -07:00
Kyle Wigley
7ec5c122e1 Merge pull request #3228 from fishtown-analytics/cleanup/makefile
Better make targets
2021-04-12 10:49:59 -04:00
Jeremy Cohen
a10ab99efc Merge pull request #3243 from fuchsst/fix/3241-add-missing-exposures-property
Fix/3241 add missing exposures property
2021-04-12 09:27:40 -04:00
Fuchs, Stefan
9f4398c557 added contribution to changelog 2021-04-11 18:26:29 +02:00
Fuchs, Stefan
d60f6bc89b added exposures property to manifest 2021-04-11 18:26:05 +02:00
Kyle Wigley
617eeb4ff7 test code changes without reinstalling everything 2021-04-08 12:07:55 -04:00
Kyle Wigley
5b55825638 add flaky logic to bigquery 2021-04-07 09:02:44 -04:00
Kyle Wigley
103d524db5 update changelog 2021-04-06 14:33:05 -04:00
Kyle Wigley
babd084a9b better make targets, some descriptions 2021-04-06 14:33:05 -04:00
Gerda Shank
749f87397e Merge pull request #3219 from fishtown-analytics/partial_parsing
Use Manifest instead of ParseResult [#3163]
2021-04-06 14:05:17 -04:00
Gerda Shank
307d47ebaf Use Manifest instead of ParseResults [#3163] 2021-04-06 13:51:43 -04:00
Jeremy Cohen
6acd4b91c1 Merge pull request #3227 from fishtown-analytics/update/changelog-0191
Update changelog for 0.19.1, 0.18.2
2021-04-05 16:36:28 -04:00
Jeremy Cohen
f4a9530894 Update changelog per 0.18.2 2021-04-05 15:11:08 -04:00
Jeremy Cohen
ab65385a16 Update changelog per 0.19.1 2021-04-05 15:08:33 -04:00
Jeremy Cohen
ebd761e3dc Merge pull request #3156 from max-sixty/patch-1
Update google cloud dependencies
2021-04-02 15:13:24 -04:00
Maximilian Roos
3b942ec790 Merge branch 'develop' into patch-1 2021-04-02 10:09:26 -07:00
Maximilian Roos
b373486908 Update CHANGELOG.md 2021-04-02 10:08:51 -07:00
Maximilian Roos
c8cd5502f6 Update setup.py 2021-04-02 10:05:16 -07:00
Maximilian Roos
d6dd968c4f Pin to major versions 2021-03-31 18:51:53 -07:00
Jeremy Cohen
b8d73d2197 Merge pull request #3182 from cgopalan/app-name-for-postgres
Set application_name for Postgres connections
2021-03-31 15:56:59 -04:00
Kyle Wigley
17e57f1e0b Merge pull request #3181 from fishtown-analytics/feature/data-test-materialization
Adding test materialization, implement for data tests
2021-03-30 14:21:43 -04:00
Kyle Wigley
e21bf9fbc7 code comments and explicit function call 2021-03-30 12:47:40 -04:00
Kyle Wigley
12e281f076 update changelog 2021-03-29 09:50:37 -04:00
Kyle Wigley
a5ce658755 first pass using materialization for data tests 2021-03-29 09:49:02 -04:00
Kyle Wigley
ce30dfa82d Merge pull request #3204 from fishtown-analytics/updates/tox
dev env clean up and improvements
2021-03-29 09:47:49 -04:00
Kyle Wigley
c04d1e9d5c fix circleci tests (forcing azure pipeline tests) 2021-03-29 09:19:05 -04:00
Kyle Wigley
80031d122c fix windows rpc tests 2021-03-29 09:19:05 -04:00
Kyle Wigley
943b090c90 debug windows tests 2021-03-29 09:19:04 -04:00
Kyle Wigley
39fd53d1f9 fix typos, set max-line-length (force azure pipeline tests) 2021-03-29 09:19:04 -04:00
Kyle Wigley
777e7b3b6d update changelog 2021-03-29 09:19:04 -04:00
Kyle Wigley
2783fe2a9f fix last CI step 2021-03-29 09:11:06 -04:00
Kyle Wigley
f5880cb001 CI tweaks 2021-03-29 09:11:06 -04:00
Kyle Wigley
26e501008a use new docker image for tests 2021-03-29 09:11:06 -04:00
Kyle Wigley
2c67e3f5c7 update tox, update makefile, run tests natively by default, general dev workflow cleanup 2021-03-29 09:11:06 -04:00
Kyle Wigley
033596021d Merge pull request #3148 from fishtown-analytics/dependabot/pip/plugins/snowflake/snowflake-connector-python-secure-local-storage--2.4.1
Bump snowflake-connector-python[secure-local-storage] from 2.3.6 to 2.4.1 in /plugins/snowflake
2021-03-29 09:09:49 -04:00
Kyle Wigley
f36c72e085 update changelog 2021-03-29 08:36:33 -04:00
Kyle Wigley
fefaf7b4be update snowflake deps 2021-03-26 16:04:48 -04:00
Chandrakant Gopalan
91431401ad Updated changelog 2021-03-26 16:01:47 -04:00
Chandrakant Gopalan
59d96c08a1 Add tests for application_name 2021-03-26 15:25:38 -04:00
Chandrakant Gopalan
f10447395b Fix tests 2021-03-25 21:18:42 -04:00
Chandrakant Gopalan
c2b6222798 Merge branch 'develop' of https://github.com/fishtown-analytics/dbt into app-name-for-postgres 2021-03-25 21:03:40 -04:00
Chandrakant Gopalan
3a58c49184 Default application_name to dbt 2021-03-25 21:03:08 -04:00
Jeremy Cohen
440a5e49e2 Merge pull request #3041 from yu-iskw/issue-3040
Pass the default scopes to the default BigQuery credentials
2021-03-24 17:59:32 -04:00
Jeremy Cohen
77c10713a3 Merge pull request #3100 from prratek/specify-cols-to-update
Gets columns to update from config for BQ and Snowflake
2021-03-22 17:55:49 +01:00
Jeremy Cohen
48e367ce2f Merge branch 'develop' into specify-cols-to-update 2021-03-22 13:56:11 +01:00
Jeremy Cohen
934c23bf39 Merge pull request #3145 from jmcarp/jmcarp/bigquery-job-labels
Parse query comment and use as bigquery job labels.
2021-03-22 13:30:08 +01:00
Chandrakant Gopalan
e0febcb6c3 Set application_name for Postgres connections 2021-03-20 13:24:44 -04:00
Joshua Carp
044a6c6ea4 Cleanups from code review. 2021-03-20 00:59:55 -04:00
Prratek Ramchandani
8ebbc10572 Merge branch 'develop' into specify-cols-to-update 2021-03-18 09:55:12 -04:00
Jeremy Cohen
7435828082 Merge pull request #3165 from cgopalan/dup-macro-message
Raise proper error message if duplicate macros found
2021-03-17 14:53:50 +01:00
Jeremy Cohen
369b595e8a Merge branch 'develop' into dup-macro-message 2021-03-17 12:46:38 +01:00
Jeremy Cohen
9a6d30f03d Merge pull request #3158 from techytushar/fix#3147
Feature to add _n alias to same column names #3147
2021-03-17 12:15:35 +01:00
Prratek Ramchandani
6bdd01d52b Merge branch 'develop' into specify-cols-to-update 2021-03-16 16:13:30 -04:00
prratek
bae9767498 add my name to contributors 2021-03-16 16:07:50 -04:00
prratek
b0e50dedb8 update changelog 2021-03-16 15:59:34 -04:00
Prratek Ramchandani
96bfb3b259 Update core/dbt/include/global_project/macros/materializations/common/merge.sql
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-03-16 15:54:04 -04:00
Prratek Ramchandani
909068dfa8 leave quoting for merge_update_columns to the user
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-03-16 15:53:25 -04:00
Chandrakant Gopalan
f4c74968be Add to contributor list 2021-03-16 13:53:51 -04:00
Chandrakant Gopalan
0e958f3704 Add to changelog 2021-03-16 13:51:42 -04:00
Chandrakant Gopalan
a8b2942f93 Fix duplicate macro path message 2021-03-16 13:40:18 -04:00
Tushar Mittal
564fe62400 Add unit tests for SQL process_results 2021-03-16 21:42:36 +05:30
Chandrakant Gopalan
5c5013191b Fix failing test 2021-03-14 17:46:53 -04:00
Chandrakant Gopalan
31989b85d1 Fix flake8 errors 2021-03-14 15:46:47 -04:00
Chandrakant Gopalan
5ed4af2372 Raise proper error message if duplicate macros found 2021-03-14 15:33:15 -04:00
prratek
4d18e391aa correct the load date for updated entries in "update seed" 2021-03-13 12:59:41 -05:00
prratek
2feeb5b927 Merge remote-tracking branch 'origin/specify-cols-to-update' into specify-cols-to-update 2021-03-13 12:26:22 -05:00
Prratek Ramchandani
2853f07875 use correct config var name
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-03-13 12:26:12 -05:00
prratek
4e6adc07a1 switch to correct data dir for second run 2021-03-13 12:25:02 -05:00
Jeremy Cohen
6a5ed4f418 Merge pull request #3161 from fishtown-analytics/pin/agate-1.6.2
Pin agate<1.6.2 to fix installation
2021-03-12 17:11:40 +01:00
Jeremy Cohen
ef25698d3d Pin agate>=1.6,<1.6.2 to fix installation 2021-03-12 12:17:53 +01:00
Tushar Mittal
429dcc7000 Update changelog 2021-03-11 22:10:57 +05:30
Gerda Shank
ab3f994626 Merge pull request #3157 from fishtown-analytics/track_resource_counts
Track resource counts
2021-03-11 09:42:09 -05:00
Tushar Mittal
5f8235fcfc Feature to add _n alias to same column names #3147
Signed-off-by: Tushar Mittal <chiragmittal.mittal@gmail.com>
2021-03-11 19:44:58 +05:30
Gerda Shank
db325d0fde Track resource counts 2021-03-10 23:07:02 -05:00
Maximilian Roos
8dc1f49ac7 Update google cloud dependencies 2021-03-10 19:57:57 -08:00
Joshua Carp
9fe2b651ed Merge branch 'develop' of github.com:fishtown-analytics/dbt into jmcarp/bigquery-job-labels 2021-03-09 23:32:11 -05:00
Jeremy Cohen
24e4b75c35 Merge pull request #3151 from bastienboutonnet/chore/bump_hologram_and_dataclasses
chore: allow dataclasses >= 0.6, < 0.9
2021-03-09 17:59:29 +01:00
Bastien Boutonnet
34174abf26 add changelog entry 2021-03-09 15:56:39 +01:00
Bastien Boutonnet
af778312cb relax dependency on dataclasses requirements as in hologram 2021-03-09 15:55:22 +01:00
Bastien Boutonnet
280f5614ef bump hologram to 0.0.14 2021-03-08 22:24:46 +01:00
Joshua Carp
8566a46793 Add BigQuery job labels to changelog. 2021-03-08 15:50:00 -05:00
Joshua Carp
af3c3f4cbe Add tests for bigquery label sanitize helper. 2021-03-08 15:43:53 -05:00
Bastien Boutonnet
034a44e625 fix git install link 2021-03-08 21:02:00 +01:00
Bastien Boutonnet
84155fdff7 point hologram install to my fork and up dataclasses 2021-03-08 20:42:16 +01:00
prratek
8255c913a3 change test logic for new seed directories 2021-03-06 19:12:33 -05:00
prratek
4d4d17669b refactor seeds directory structure and names 2021-03-06 19:08:23 -05:00
prratek
540a0422f5 modify seeds to contain load date and some modified records 2021-03-06 19:06:35 -05:00
prratek
de4d7d6273 Revert "modify some records and the expected result"
This reverts commit 1345d955
2021-03-06 18:39:35 -05:00
prratek
1345d95589 modify some records and the expected result 2021-03-06 18:30:50 -05:00
prratek
a5bc19dd69 paste in some data for seeds 2021-03-06 18:27:02 -05:00
prratek
25b143c8cc WIP test case and empty seeds 2021-03-06 18:17:07 -05:00
Joshua Carp
82cca959e4 Merge branch 'develop' of github.com:fishtown-analytics/dbt into jmcarp/bigquery-job-labels 2021-03-05 09:39:48 -05:00
dependabot[bot]
d52374a0b6 Bump snowflake-connector-python[secure-local-storage]
Bumps [snowflake-connector-python[secure-local-storage]](https://github.com/snowflakedb/snowflake-connector-python) from 2.3.6 to 2.4.1.
- [Release notes](https://github.com/snowflakedb/snowflake-connector-python/releases)
- [Commits](https://github.com/snowflakedb/snowflake-connector-python/compare/v2.3.6...v2.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-05 06:15:17 +00:00
Joshua Carp
c71a18ca07 Hyphenate query comment fields and fix deserialization bug. 2021-03-05 00:09:17 -05:00
Joshua Carp
8d73ae2cc0 Address comments from code review. 2021-03-04 10:20:15 -05:00
Joshua Carp
7b0c74ca3e Fix lint. 2021-03-04 00:34:46 -05:00
Joshua Carp
62be9f9064 Sanitize bigquery labels. 2021-03-04 00:14:50 -05:00
Joshua Carp
2fdc113d93 Parse query comment and use as bigquery job labels. 2021-03-04 00:06:59 -05:00
Gerda Shank
b70fb543f5 Merge pull request #3138 from fishtown-analytics/mashumaro-cleanup
Use updated Mashumaro code
2021-03-03 15:46:56 -05:00
Gerda Shank
31c88f9f5a Use updated Mashumaro code 2021-03-03 13:39:51 -05:00
Prratek Ramchandani
af3a818f12 loop over column_name instead of column.name
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-03-03 09:27:05 -05:00
prratek
a07532d4c7 revert changes to incremental materializations 2021-03-02 22:14:58 -05:00
prratek
fb449ca4bc rename new config var to merge_update_columns 2021-03-02 22:11:12 -05:00
prratek
4da65643c0 use merge_update_columns when getting merge sql 2021-03-02 22:10:09 -05:00
Prratek Ramchandani
bf64db474c fix typo
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-03-02 21:59:21 -05:00
Kyle Wigley
344a14416d Merge pull request #3065 from fishtown-analytics/feature/write-artifact-schema
Collect and write json schema for dbt artifacts
2021-02-22 12:47:35 -05:00
Jeremy Cohen
be47a0c5db Merge pull request #3117 from fishtown-analytics/fix/deps-git-warn-if-main-master
git package: warn if specified revision master, main
2021-02-22 10:49:31 +01:00
prratek
808b980301 move "update cols" incremental test to snowflake models 2021-02-20 14:40:55 -05:00
prratek
3528480562 test incremental model w/ subset of cols to update 2021-02-19 23:17:00 -05:00
prratek
6bd263d23f add incremental_update_columns to Snowflake & BQ config schemas 2021-02-19 21:17:39 -05:00
prratek
2b9aa3864b rename config field to incremental_update_columns 2021-02-19 21:13:05 -05:00
Prratek Ramchandani
81155caf88 use get_columns_in_relation as default for snowflake
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-02-19 21:10:34 -05:00
Jeremy Cohen
c7c057483d sry flake8 2021-02-19 15:19:03 +01:00
Jeremy Cohen
7f5170ae4d Warn if main or master is specified, too 2021-02-19 11:19:36 +01:00
Jeremy Cohen
49b8693b11 Merge pull request #3104 from VasiliiSurov/develop
3057 Moving from 'master' to 'HEAD' default branch in git
2021-02-18 20:29:34 +01:00
Jeremy Cohen
d7b0a14eb5 Merge branch 'develop' into develop 2021-02-18 19:55:54 +01:00
Jeremy Cohen
8996cb1e18 Merge pull request #2976 from pcasteran/fix/2940-bq-incremental-var-declaration
Fix `_dbt_max_partition` declaration and initialization for BigQuery incremental models
2021-02-18 18:54:16 +01:00
Jeremy Cohen
38f278cce0 Merge pull request #3111 from fishtown-analytics/feature/architecture-md
Add ARCHITECTURE.md
2021-02-18 14:55:05 +01:00
Jeremy Cohen
bb4e475044 Add aARCHITECTURE.md [skip ci] 2021-02-17 14:58:43 +01:00
Pascal Casteran
4fbe36a8e9 Moved changelog entry up to v0.20.0 2021-02-16 19:12:22 +01:00
Pascal Casteran
a1a40b562a Updated CHANGELOG.md 2021-02-16 19:09:22 +01:00
Pascal Casteran
3a4a1bb005 Fix _dbt_max_partition declaration and initialization for BigQuery incremental models 2021-02-16 19:09:22 +01:00
Prratek Ramchandani
4f8c10c1aa default to get_columns_in_relation if not specified in config
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-02-15 20:58:39 -05:00
VS
4833348769 Merge remote-tracking branch 'origin/develop' into develop 2021-02-15 17:06:43 -05:00
VS
ad07d59a78 3057 Changelog.md 2021-02-15 17:06:36 -05:00
VasiliiSurov
e8aaabd1d3 Merge pull request #1 from VasiliiSurov/3057_dbt_deps
3057 Replacing 'master' to 'HEAD' for default git commit
2021-02-15 17:02:13 -05:00
Kyle Wigley
d7d7396eeb update dependabot config, turn off rebasing 2021-02-15 14:42:44 -05:00
Kyle Wigley
41538860cd Revert "Merge branch '0.19.latest' into develop"
This reverts commit 5c9f8a0cf0, reversing
changes made to 36d1bddc5b.
2021-02-15 13:26:18 -05:00
Kyle Wigley
5c9f8a0cf0 Merge branch '0.19.latest' into develop 2021-02-15 12:11:52 -05:00
Github Build Bot
11c997c3e9 Merge remote-tracking branch 'origin/releases/0.19.1b2' into 0.19.latest 2021-02-15 17:04:12 +00:00
Github Build Bot
1b1184a5e1 Release dbt v0.19.1b2 2021-02-15 17:00:50 +00:00
Kyle Wigley
4ffcc43ed9 Merge pull request #3101 from fishtown-analytics/vendor-mashumaro
Vendor mashumaro code with dbt specific modifications
2021-02-15 10:36:10 -05:00
Kyle Wigley
4ccaac46a6 Update core/mashumaro/types.py
Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-02-15 10:02:50 -05:00
Kyle Wigley
ba88b84055 Update core/mashumaro/serializer/base/dict.py
Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2021-02-15 10:02:45 -05:00
prratek
9086634c8f get columns to update from config for BQ and Snowflake 2021-02-13 11:09:25 -05:00
VS
e88f1f1edb 3057 Replacing 'master' to 'HEAD' for default git commit 2021-02-12 22:34:49 -05:00
Kyle Wigley
13c7486f0e add license info 2021-02-12 17:32:15 -05:00
Kyle Wigley
8e811ba141 vendor mashumaro code in dbt, include as another module in dbt-core 2021-02-12 17:19:28 -05:00
Github Build Bot
c5d86afed6 Merge remote-tracking branch 'origin/releases/0.19.1b1' into 0.19.latest 2021-02-12 16:10:31 +00:00
Github Build Bot
43a0cfbee1 Release dbt v0.19.1b1 2021-02-12 16:06:52 +00:00
Jeremy Cohen
8567d5f302 Fix int64, ts partitions. Rework tests 2021-02-12 15:58:47 +01:00
Jeremy Cohen
36d1bddc5b Merge pull request #3098 from fishtown-analytics/fix/bq-insert-overwrite-int-ts-partitions
[BQ] Fix insert_overwrite with int + ts partitions
2021-02-12 15:56:59 +01:00
Kyle Wigley
bf992680af Merge pull request #3090 from fishtown-analytics/fix/dbt-mashumaro-dep
Move dbt-mashumaro dep to setup.py to prep for beta release
2021-02-12 09:32:31 -05:00
Kyle Wigley
e064298dfc move dbt-mashumaro dep to setup.py to prep for beta release 2021-02-12 08:51:14 -05:00
Jeremy Cohen
e01a10ced5 Fix int64, ts partitions. Rework tests 2021-02-12 14:03:03 +01:00
Gerda Shank
2aa10fb1ed Use version 0.0.13 of Hologram 2021-02-11 11:48:58 -05:00
Gerda Shank
66f442ad76 Merge pull request #3071 from fishtown-analytics/hologram-version-0.0.13
Use version 0.0.13 of Hologram
2021-02-11 11:46:57 -05:00
Kyle Wigley
11f1ecebcf update changelog 2021-02-11 11:21:40 -05:00
Kyle Wigley
e339cb27f6 first pass at writing out artifact schema to destination 2021-02-11 11:20:38 -05:00
Kyle Wigley
bce3232b39 Merge pull request #3062 from fishtown-analytics/feature/dependabot-config
Add dependabot config
2021-02-11 10:22:13 -05:00
Kyle Wigley
b08970ce39 Merge pull request #3069 from fishtown-analytics/fix/update-docs
Update docs with permalinks and new git branching strategy
2021-02-11 10:17:57 -05:00
Gerda Shank
533f88ceaf Use version 0.0.13 of Hologram 2021-02-10 14:08:27 -05:00
Jeremy Cohen
c8f0469a44 Merge pull request #3036 from ran-eh/patch-2
Fix incorrect error message (trivial)
2021-02-10 12:00:43 +01:00
Kyle Wigley
a1fc24e532 update docs with permalinks and new git branching 2021-02-10 00:31:40 -05:00
Ran Ever-Hadani
d80daa48df Fix flake8 error
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-02-09 14:07:50 -08:00
Kyle Wigley
92aae2803f update changelog 2021-02-09 09:53:32 -05:00
Gerda Shank
77cbbbfaf2 Performance fixes, including supporting libyaml, caching
mapped_fields in the classes for 'from_dict', removing deepcopy
on fqn_search, separating validation from 'from_dict',
and special handling for dbt internal not_null and unique tests.
Use TestMacroNamespace instead of original in order to limit
the number of macros in the context.  Integrate mashumaro into
dbt to improve performance of 'from_dict' and 'to_dict'
2021-02-08 14:06:14 -05:00
Gerda Shank
6c6649f912 Performance fixes, including supporting libyaml, caching
mapped_fields in the classes for 'from_dict', removing deepcopy
on fqn_search, separating validation from 'from_dict',
and special handling for dbt internal not_null and unique tests.
Use TestMacroNamespace instead of original in order to limit
the number of macros in the context.  Integrate mashumaro into
dbt to improve performance of 'from_dict' and 'to_dict'
2021-02-05 15:23:55 -05:00
Yu ISHIKAWA
55fbaabfda Pass the default scopes to the default BigQuery credentials 2021-01-29 18:09:05 +09:00
Ran Ever-Hadani
56c2518936 Correct message to be more general
as per https://github.com/fishtown-analytics/dbt/pull/3036#pullrequestreview-578328868
2021-01-28 11:22:14 -08:00
Kyle Wigley
2b48152da6 Merge branch 'dev/0.19.1' into dev/margaret-mead 2021-01-27 17:16:13 -05:00
Christophe Blefari
e743e23d6b Update CHANGELOG to release fix in dbt 0.19.1 version 2021-01-27 16:57:29 -05:00
Christophe Blefari
f846f921f2 Bump werkzeug upper bound dependency constraint to include version 1.0 2021-01-27 16:55:56 -05:00
Ran Ever-Hadani
e52a599be6 Add fix 2021-01-27 13:52:01 -08:00
Ran Ever-Hadani
99744bd318 Fix incorrect error message (trivial) 2021-01-27 13:09:32 -08:00
Github Build Bot
1060035838 Merge remote-tracking branch 'origin/releases/0.19.0' into dev/kiyoshi-kuromiya 2021-01-27 18:02:37 +00:00
Github Build Bot
69cc20013e Release dbt v0.19.0 2021-01-27 17:39:48 +00:00
Github Build Bot
3572bfd37d Merge remote-tracking branch 'origin/releases/0.19.0rc3' into dev/kiyoshi-kuromiya 2021-01-27 16:42:46 +00:00
Github Build Bot
a6b82990f5 Release dbt v0.19.0rc3 2021-01-27 16:07:41 +00:00
Kyle Wigley
540c1fd9c6 Merge pull request #3019 from fishtown-analytics/fix/cleanup-dockerfile
Clean up docker resources
2021-01-25 10:19:45 -05:00
Jeremy Cohen
46d36cd412 Merge pull request #3028 from NiallRees/lowercase_cte_names
Make generated CTE test names lowercase to match style guide
2021-01-25 14:39:26 +01:00
NiallRees
a170764fc5 Add to contributors 2021-01-25 11:16:00 +00:00
NiallRees
f72873a1ce Update CHANGELOG.md
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-01-25 11:13:32 +00:00
NiallRees
82496c30b1 Changelog 2021-01-24 16:35:40 +00:00
NiallRees
cb3c007acd Make generated CTE test names lowercase to match style guide 2021-01-24 16:19:20 +00:00
Jeremy Cohen
cb460a797c Merge pull request #3018 from lynxcare/fix-issue-debug-exit-code
dbt debug should return 1 when one of the tests fail
2021-01-21 16:36:03 +01:00
Kyle Wigley
1b666d01cf add a dependabot config, hopefully this ignores docker/requirements/*.txt files 2021-01-21 09:51:35 -05:00
Sam Debruyn
df24c7d2f8 Merge branch 'dev/margaret-mead' into fix-issue-debug-exit-code 2021-01-21 15:39:18 +01:00
Sam Debruyn
133c15c0e2 move in changelog to v0.20 2021-01-21 15:38:31 +01:00
Kyle Wigley
116e18a19e rename testing dockerfile 2021-01-21 09:28:17 -05:00
Sam Debruyn
ec0af7c97b remove exitcodes and sys.exit 2021-01-21 10:36:05 +01:00
Jeremy Cohen
a34a877737 Merge pull request #2974 from rvacaru/fix-bug-2731
Fix bug #2731 on stripping query comments for snowflake
2021-01-21 09:54:22 +01:00
Sam Debruyn
f018794465 fix flake test - formatting 2021-01-20 21:09:58 +01:00
Sam Debruyn
d45f5e9791 add missing conditions 2021-01-20 18:15:32 +01:00
Razvan Vacaru
04bd0d834c added extra unit test 2021-01-20 18:06:17 +01:00
Sam Debruyn
ed4f0c4713 formatting 2021-01-20 18:04:21 +01:00
Sam Debruyn
c747068d4a use sys.exit 2021-01-20 16:51:06 +01:00
Kyle Wigley
aa0fbdc993 update changelog 2021-01-20 10:33:18 -05:00
Kyle Wigley
b50bfa7277 - rm older dockerfiles
- add dockerfile from dbt-releases
- rename the development dockerfile to Dockerfile.dev to avoid confusion
2021-01-20 10:23:03 -05:00
Sam Debruyn
e91988f679 use ExitCodes enum for exit code 2021-01-20 16:09:41 +01:00
Sam Debruyn
3ed1fce3fb update changelog 2021-01-20 16:06:24 +01:00
Sam Debruyn
e3ea0b511a dbt debug should return 1 when one of the tests fail 2021-01-20 16:00:58 +01:00
Razvan Vacaru
c411c663de moved unit tests and updated changelog.md 2021-01-19 19:04:58 +01:00
Razvan Vacaru
1c6f66fc14 Merge branch 'dev/margaret-mead' of https://github.com/fishtown-analytics/dbt into fix-bug-2731 2021-01-19 19:01:01 +01:00
Jeremy Cohen
1f927a374c Merge pull request #2928 from yu-iskw/issue-1843
Support require_partition_filter and partition_expiration_days in BQ
2021-01-19 12:11:39 +01:00
Jeremy Cohen
07c4225aa8 Merge branch 'dev/margaret-mead' into issue-1843 2021-01-19 11:24:59 +01:00
Github Build Bot
42a85ac39f Merge remote-tracking branch 'origin/releases/0.19.0rc2' into dev/kiyoshi-kuromiya 2021-01-14 17:41:49 +00:00
Github Build Bot
16e6d31ee3 Release dbt v0.19.0rc2 2021-01-14 17:21:25 +00:00
Kyle Wigley
a6db5b436d Merge pull request #2996 from fishtown-analytics/fix/rm-ellipses
Remove ellipses printed while parsing
2021-01-14 10:39:16 -05:00
Kyle Wigley
47675f2e28 update changelog 2021-01-14 09:28:28 -05:00
Kyle Wigley
0642bbefa7 remove ellipses printed while parsing 2021-01-14 09:28:05 -05:00
Kyle Wigley
43da603d52 Merge pull request #3009 from fishtown-analytics/fix/exposure-parsing
Fix exposure parsing to allow other resources with the same name
2021-01-14 09:26:02 -05:00
Kyle Wigley
f9e1f4d111 update changelog 2021-01-13 11:54:20 -05:00
Jeremy Cohen
1508564e10 Merge pull request #3008 from fishtown-analytics/feat/print-exposure-stats-too
Add exposures to print_compile_stats
2021-01-13 15:58:13 +01:00
Kyle Wigley
c14e6f4dcc add test for dupe exposures and dupe model/exposure name 2021-01-13 08:55:22 -05:00
Jeremy Cohen
75b6a20134 Add exposures to Found list 2021-01-12 19:07:52 +01:00
Kyle Wigley
d82a07c221 tweak exposure parsing logic 2021-01-12 12:41:51 -05:00
Jeremy Cohen
c6f7dbcaa5 Merge pull request #3006 from stpierre/postgres-unpin-botocore
postgres: Don't pin botocore version
2021-01-12 13:59:55 +01:00
Chris St. Pierre
82cd099e48 Update CHANGELOG 2021-01-12 06:20:09 -06:00
Chris St. Pierre
546c011dd8 postgres: Don't pin botocore version
`snowflake-connector-python` doesn't pin it, and it restricts us to a
much older version of boto3 than the boto3 pin would otherwise allow
(specifically, botocore<1.15 requires boto3<1.12).
2021-01-11 17:25:03 -06:00
Jeremy Cohen
10b33ccaf6 Merge pull request #3004 from mikaelene/Snapshot_merge_WHEN_MATCHED
This changes makes the macro easier to read and workable on SQL Server
2021-01-11 16:42:09 +01:00
mikaelene
bc01572176 Sane as #3003. But for postgres 2021-01-11 16:04:38 +01:00
mikaelene
ccd2064722 This changes makes the macro easier to read and makes the code work for SQL Server without a custom adapter macro. Solved #3003 2021-01-11 15:04:23 +01:00
mikaelene
0fb42901dd This changes makes the macro easier to read and makes the code work for SQL Server without a custom adapter macro. Solved #3003 2021-01-11 14:58:07 +01:00
Jeremy Cohen
a4280d7457 Merge pull request #3000 from swanderz/tsql_not_equal_workaround
Tsql not equal workaround
2021-01-11 09:40:33 +01:00
Anders
6966ede68b Update CHANGELOG.md
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-01-10 20:54:37 -08:00
Anders
27dd14a5a2 Update core/dbt/include/global_project/macros/materializations/snapshot/strategies.sql
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-01-10 20:54:10 -08:00
Anders
2494301f1e Update CHANGELOG.md
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2021-01-10 20:53:52 -08:00
Anders Swanson
f13143accb for posterity 2021-01-08 13:23:13 -08:00
Anders Swanson
26d340a917 temp hack 2021-01-08 12:14:08 -08:00
Anders Swanson
cc75cd4102 no tsql support for condA != condB 2021-01-08 12:10:15 -08:00
Anders Swanson
cf8615b231 Merge branch 'dev/kiyoshi-kuromiya' of https://github.com/fishtown-analytics/dbt into dev/kiyoshi-kuromiya 2021-01-08 12:03:15 -08:00
Jeremy Cohen
30f473a2b1 Merge pull request #2994 from fishtown-analytics/copyedit-changelog
Light cleanup of v0.19.0 changelogs
2021-01-07 16:00:02 +01:00
Jeremy Cohen
4618709baa Lightly edit v0.19 changelogs 2021-01-07 10:13:43 +01:00
Razvan Vacaru
16b098ea42 updated CHANGELOG.md 2021-01-04 17:43:03 +01:00
Razvan Vacaru
b31c4d407a Fix #2731 stripping snowflake comments in multiline queries 2021-01-04 17:41:00 +01:00
Kyle Wigley
28c36cc5e2 Merge pull request #2988 from fishtown-analytics/fix/dockerfile
Manually fix requirements for dockerfile using new pip version
2021-01-04 09:10:05 -05:00
Kyle Wigley
6bfbcb842e manually fix dockerfile using new pip version 2020-12-31 13:53:50 -05:00
Github Build Bot
a0eade4fdd Merge remote-tracking branch 'origin/releases/0.19.0rc1' into dev/kiyoshi-kuromiya 2020-12-29 23:07:35 +00:00
Github Build Bot
ee24b7e88a Release dbt v0.19.0rc1 2020-12-29 22:52:34 +00:00
Anders Swanson
c9baddf9a4 Merge branch 'master' of https://github.com/fishtown-analytics/dbt into dev/kiyoshi-kuromiya 2020-12-22 23:11:09 -08:00
Kyle Wigley
c5c780a685 Merge pull request #2972 from fishtown-analytics/feature/update-dbt-docs
dbt-docs changes for v0.19.0-rc1
2020-12-22 14:07:20 -05:00
Kyle Wigley
421aaabf62 Merge pull request #2961 from fishtown-analytics/feature/add-adapter-query-stats
Include adapter response info in execution results
2020-12-22 13:57:07 -05:00
Kyle Wigley
86788f034f update changelog 2020-12-22 13:30:50 -05:00
Kyle Wigley
232d3758cf update dbt docs 2020-12-22 13:17:51 -05:00
Kyle Wigley
71bcf9b31d update changelog 2020-12-22 13:08:12 -05:00
Kyle Wigley
bf4ee4f064 update api, fix tests, add placeholder for test/source results 2020-12-22 12:13:37 -05:00
Kyle Wigley
aa3bdfeb17 update naming 2020-12-21 13:35:15 -05:00
Jeremy Cohen
ce6967d396 Merge pull request #2966 from fishtown-analytics/fix/add-ctes-comment
Update comments for _add_ctes()
2020-12-18 10:54:37 -05:00
Yu ISHIKAWA
330065f5e0 Add a condition for require_partition_filter 2020-12-18 11:14:03 +09:00
Yu ISHIKAWA
944db82553 Remove unnecessary code for print debug 2020-12-18 11:14:03 +09:00
Yu ISHIKAWA
c257361f05 Fix syntax 2020-12-18 11:14:03 +09:00
Yu ISHIKAWA
ffdbfb018a Implement tests in test_bigquery_changing_partitions.py 2020-12-18 11:14:01 +09:00
Yu ISHIKAWA
cfa2bd6b08 Remove tests fromm test_bigquery_adapter_specific.py 2020-12-18 11:13:16 +09:00
Yu ISHIKAWA
51e90c3ce0 Format 2020-12-18 11:13:16 +09:00
Yu ISHIKAWA
d69149f43e Update 2020-12-18 11:13:15 +09:00
Yu ISHIKAWA
f261663f3d Add debug code 2020-12-18 11:13:15 +09:00
Yu ISHIKAWA
e5948dd1d3 Update 2020-12-18 11:13:15 +09:00
Yu ISHIKAWA
5f13aab7d8 Print debug 2020-12-18 11:13:15 +09:00
Yu ISHIKAWA
292d489592 Format code 2020-12-18 11:13:15 +09:00
Yu ISHIKAWA
0a01f20e35 Update CHANGELOG.md 2020-12-18 11:13:11 +09:00
Yu ISHIKAWA
2bd08d5c4c Support require_partition_filter and partition_expiration_days in BQ 2020-12-18 11:12:47 +09:00
Jeremy Cohen
adae5126db Merge pull request #2954 from fishtown-analytics/feature/defer-tests
Feature: defer tests
2020-12-17 18:01:14 -05:00
Kyle Wigley
dddf1bcb76 first pass at adding query stats, naming tbd 2020-12-17 16:39:02 -05:00
Jeremy Cohen
d23d4b0fd4 Merge pull request #2963 from tyang209/issue-2931
Bumped boto3 version uppper range for dbt-redshift
2020-12-17 14:30:47 -05:00
Tao Yang
658f7550b3 Merge branch 'dev/kiyoshi-kuromiya' into issue-2931 2020-12-17 08:58:49 -08:00
Kyle Wigley
cfb50ae21e Merge pull request #2960 from fishtown-analytics/feature/python-39
Test python3.9
2020-12-17 11:11:56 -05:00
Jeremy Cohen
9b0a365822 Update comments for _add_ctes() 2020-12-17 10:35:04 -05:00
Jeremy Cohen
97ab130619 Merge pull request #2958 from fishtown-analytics/fix/keyerror-defer-missing-parent
Fix KeyError from defer + deletion
2020-12-17 10:29:51 -05:00
Tao Yang
3578fde290 Bumped boto3 version uppper range for dbt-redshift 2020-12-16 20:03:53 -08:00
Jeremy Cohen
f382da69b8 Changelog 2020-12-16 17:46:00 -05:00
Jeremy Cohen
2da3d215c6 Add test case to repro bug 2020-12-16 17:38:27 -05:00
Kyle Wigley
43ed29c14c update changelog 2020-12-16 16:29:48 -05:00
Jeremy Cohen
9df0283689 Truthier? 2020-12-16 14:55:27 -05:00
Jeremy Cohen
04b82cf4a5 What is backward may not be forward 2020-12-16 14:55:27 -05:00
Jeremy Cohen
274c3012b0 Add defer to rpc test method 2020-12-16 14:53:25 -05:00
Jeremy Cohen
2b24a4934f defer tests, too 2020-12-16 14:42:00 -05:00
Kyle Wigley
692a423072 comment out snowflake py39 tests 2020-12-16 11:27:00 -05:00
Kyle Wigley
148f55335f address issue with py39 2020-12-16 11:25:31 -05:00
Kyle Wigley
2f752842a1 update hologram and add new envs to tox 2020-12-16 11:25:31 -05:00
Jeremy Cohen
aff72996a1 Merge pull request #2946 from fishtown-analytics/fix/defer-if-not-exist
Defer iff unselected reference does not exist in current env
2020-12-16 11:22:31 -05:00
Jeremy Cohen
08e425bcf6 Handle keyerror if old node missing 2020-12-16 00:24:00 -05:00
Kyle Wigley
454ddc601a Merge pull request #2943 from fishtown-analytics/feature/refactor-run-results
Clean up run results
2020-12-15 12:42:22 -05:00
Jeremy Cohen
b025f208a8 Check if relation exists before deferring 2020-12-14 22:21:43 -05:00
Kyle Wigley
b60e533b9d fix printer output 2020-12-14 19:50:17 -05:00
Kyle Wigley
37af0e0d59 update changelog 2020-12-14 16:28:23 -05:00
Kyle Wigley
ac1de5bce9 more updates 2020-12-14 16:28:23 -05:00
Kyle Wigley
ef7ff55e07 flake8 2020-12-14 16:28:23 -05:00
Kyle Wigley
608db5b982 code cleanup + swap node with unique_id 2020-12-14 16:28:23 -05:00
Kyle Wigley
8dd69efd48 address test failures 2020-12-14 16:28:23 -05:00
Kyle Wigley
73f7fba793 fix printing test status 2020-12-14 16:28:23 -05:00
Kyle Wigley
867e2402d2 chugging along 2020-12-14 16:28:23 -05:00
Kyle Wigley
a3b9e61967 first pass, lots of TODO's [skip ci] 2020-12-14 16:28:22 -05:00
Jeremy Cohen
cd149b68e8 Merge pull request #2920 from joellabes/2913-docs-block-exposures
Render docs blocks in exposures
2020-12-13 18:38:23 -05:00
Joel Labes
cd3583c736 Merge branch 'dev/kiyoshi-kuromiya' into 2913-docs-block-exposures 2020-12-13 14:27:37 +13:00
Joel Labes
441f86f3ed Add test.notebook_info to expected manifest 2020-12-13 14:25:37 +13:00
Joel Labes
f62bea65a1 Move model.test.view_summary to parent map instead of child map 2020-12-13 14:11:04 +13:00
Jeremy Cohen
886b574987 Merge pull request #2939 from fishtown-analytics/fix/big-seed-smaller-path
Use diff file path for big seed checksum
2020-12-07 11:18:15 -05:00
Joel Labes
2888bac275 Merge branch 'dev/kiyoshi-kuromiya' into 2913-docs-block-exposures 2020-12-07 21:17:21 +13:00
Joel Labes
35c9206916 Fix test failure (?) 2020-12-07 21:15:44 +13:00
Joel Labes
c4c5b59312 Stab at updating parent and child maps 2020-12-07 17:45:12 +13:00
Jeremy Cohen
f25fb4e5ac Use diff file path for big seed checksum 2020-12-04 17:04:27 -05:00
Jeremy Cohen
868bfec5e6 Merge pull request #2907 from max-sixty/raise
Remove duplicate raise
2020-12-03 14:17:58 -05:00
Jeremy Cohen
e7c242213a Merge pull request #2908 from max-sixty/bq-default-project
Allow BigQuery to default on project name
2020-12-03 14:17:02 -05:00
Jeremy Cohen
862552ead4 Merge pull request #2930 from fishtown-analytics/revert-2858-dependabot/pip/docker/requirements/cryptography-3.2
Revert dependabot cryptography upgrade for old versions
2020-12-03 13:58:26 -05:00
Jeremy Cohen
9d90e0c167 tiny changelog fixup 2020-12-03 13:27:46 -05:00
Jeremy Cohen
a281f227cd Revert "Bump cryptography from 2.9.2 to 3.2 in /docker/requirements" 2020-12-03 12:12:15 -05:00
Maximilian Roos
5b981278db changelog 2020-12-02 14:59:35 -08:00
Maximilian Roos
c1091ed3d1 Merge branch 'dev/kiyoshi-kuromiya' into bq-default-project 2020-12-02 14:55:27 -08:00
Maximilian Roos
08aed63455 Formatting 2020-12-02 11:19:02 -08:00
Maximilian Roos
90a550ee4f Update plugins/bigquery/dbt/adapters/bigquery/connections.py
Co-authored-by: Kyle Wigley <kwigley44@gmail.com>
2020-12-02 10:41:20 -08:00
Jeremy Cohen
34869fc2a2 Merge pull request #2922 from plotneishestvo/snowflake_connector_upgrade
update cryptography package and snowflake connector
2020-12-02 12:34:34 -05:00
Pavel Plotnikov
3deb10156d Merge branch 'dev/kiyoshi-kuromiya' into snowflake_connector_upgrade 2020-12-02 12:46:02 +02:00
Maximilian Roos
8c0e84de05 Move method to module func 2020-12-01 16:19:20 -08:00
Joel Labes
23be083c39 Change models folder to ref_models folder 2020-12-02 11:59:21 +13:00
Joel Labes
217aafce39 Add line break to description, fix refs and maybe fix original_file_path 2020-12-02 11:47:29 +13:00
Joel Labes
03210c63f4 Blank instead of none description 2020-12-02 10:57:47 +13:00
Joel Labes
a90510f6f2 Ref a model that actually exists 2020-12-02 10:40:34 +13:00
Joel Labes
36d91aded6 Empty description for minimal/basic exposure object tests 2020-12-01 17:56:55 +13:00
Joel Labes
9afe8a1297 Default to empty string for ParsedExposure description 2020-12-01 17:35:42 +13:00
Maximilian Roos
1e6f272034 Add test config 2020-11-30 20:06:06 -08:00
Maximilian Roos
a1aa2f81ef _ 2020-11-30 19:30:07 -08:00
Maximilian Roos
62899ef308 _ 2020-11-30 16:54:21 -08:00
Joel Labes
7f3396c002 Forgot another comma 🤦 2020-12-01 12:46:26 +13:00
Joel Labes
453bc18196 Merge branch '2913-docs-block-exposures' of https://github.com/joellabes/dbt into 2913-docs-block-exposures 2020-12-01 12:42:11 +13:00
Joel Labes
dbb6b57b76 Forgot a comma 2020-12-01 12:40:51 +13:00
Joel Labes
d7137db78c Merge branch 'dev/kiyoshi-kuromiya' into 2913-docs-block-exposures 2020-12-01 12:34:29 +13:00
Joel Labes
5ac4f2d80b Move description arg to be below default-free args 2020-12-01 12:33:08 +13:00
Jeremy Cohen
5ba5271da9 Merge pull request #2903 from db-magnus/bq-hourly-part
Hourly, monthly and yearly partitions in BigQuery
2020-11-30 09:46:36 -05:00
Pavel Plotnikov
b834e3015a update changelog md 2020-11-30 14:46:51 +02:00
Joel Labes
c8721ded62 Code review: non-optional description, docs block tests, yaml exposure attributes 2020-11-30 20:29:47 +13:00
Magnus Fagertun
1e97372d24 Update test/unit/test_bigquery_adapter.py
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-11-30 07:26:36 +01:00
Magnus Fagertun
fd4e111784 Update test/unit/test_bigquery_adapter.py
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-11-30 00:44:25 +01:00
Magnus Fagertun
75094e7e21 Update test/unit/test_bigquery_adapter.py
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-11-30 00:44:15 +01:00
Joel Labes
8db2d674ed Update CHANGELOG.md 2020-11-28 15:08:13 +13:00
Pavel Plotnikov
ffb140fab3 update cryptography package and snowflake connector 2020-11-27 16:52:13 +02:00
Joel Labes
e93543983c Follow Jeremy's wild speculation 2020-11-27 22:45:31 +13:00
Magnus Fagertun
0d066f80ff added test and enhancements from jtcohen6 2020-11-25 21:41:51 +01:00
Magnus Fagertun
ccca1b2016 Update plugins/bigquery/dbt/adapters/bigquery/impl.py
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-11-25 21:17:07 +01:00
Kyle Wigley
fec0e31a25 Merge pull request #2902 from fishtown-analytics/fix/test-selection
set default `materialized` for test node configs
2020-11-24 12:19:40 -05:00
Kyle Wigley
d246aa8f6d update readme 2020-11-24 10:40:01 -05:00
Maximilian Roos
66bfba2258 flake8 seems to sometimes be applied 2020-11-23 17:39:57 -08:00
Maximilian Roos
b53b4373cb Definet database exclusively in contracts/connection.py 2020-11-23 17:32:41 -08:00
Maximilian Roos
0810f93883 Allow BigQuery to default on project name 2020-11-23 16:58:54 -08:00
Maximilian Roos
a4e696a252 Remove duplicate raise 2020-11-23 15:34:43 -08:00
Jeremy Cohen
0951d08f52 Merge pull request #2877 from max-sixty/unlock-google-api
Wider google-cloud dependencies
2020-11-23 14:16:12 -05:00
Jeremy Cohen
dbf367e070 Merge branch 'dev/kiyoshi-kuromiya' into unlock-google-api 2020-11-23 11:46:07 -05:00
Magnus Fagertun
6447ba8ec8 whitespace cleanup 2020-11-22 10:00:10 +01:00
Magnus Fagertun
43e260966f uppercase and lowercase for date partitions supported 2020-11-21 01:21:07 +01:00
Magnus Fagertun
b0e301b046 typo in _partitions_match 2020-11-21 00:40:27 +01:00
Magnus Fagertun
c8a9ea4979 added month,year to date partitioning, granularity comparison to _partitions_match 2020-11-21 00:24:20 +01:00
Maximilian Roos
afb7fc05da Changelog 2020-11-20 14:58:46 -08:00
Magnus Fagertun
14124ccca8 added tests for datetime and timestamp 2020-11-20 00:10:15 +01:00
Magnus Fagertun
df5022dbc3 moving granularity to render, not to break tests 2020-11-19 18:51:05 +01:00
Magnus Fagertun
015e798a31 more BQ partitioning 2020-11-19 17:42:27 +01:00
Kyle Wigley
c19125bb02 Merge pull request #2893 from fishtown-analytics/feature/track-parse-time
Add event tracking for project parse/load time
2020-11-19 10:30:46 -05:00
Kyle Wigley
0e6ac5baf1 can we just default materialization to test? 2020-11-19 09:27:31 -05:00
Magnus Fagertun
2c8d1b5b8c Added hour, year, month partitioning BQ 2020-11-19 13:47:42 +01:00
Kyle Wigley
f7c0c1c21a fix tests 2020-11-18 17:21:41 -05:00
Kyle Wigley
4edd98f7ce update changelog 2020-11-18 16:19:58 -05:00
Kyle Wigley
df0abb7000 flake8 fixes 2020-11-18 16:19:58 -05:00
Kyle Wigley
4f93da307f add event to track loading time 2020-11-18 16:19:58 -05:00
Gerda Shank
a8765d54aa Merge pull request #2895 from fishtown-analytics/string_selectors
convert cli-style strings in selectors to normalized dictionaries
2020-11-18 15:53:23 -05:00
Gerda Shank
bb834358d4 convert cli-style strings in selectors to normalized dictionaries
[#2879]
2020-11-18 14:43:44 -05:00
Jeremy Cohen
ec0f3d22e7 Merge pull request #2892 from rsella/dev/kiyoshi-kuromiya
Change dbt list command to always return 0 as exit code
2020-11-17 11:12:55 -05:00
Riccardo Sella
009b75cab6 Fix changelog and edit additional failing tests 2020-11-17 16:38:28 +01:00
Riccardo Sella
d64668df1e Change dbt list command to always return 0 as exit code 2020-11-17 14:49:24 +01:00
Gerda Shank
72e808c9a7 Merge pull request #2889 from fishtown-analytics/dbt-test-runner
Add scripts/dtr.py, dbt test runner. Bump hologram version.
2020-11-15 20:06:28 -05:00
Gerda Shank
96cc9223be Add scripts/dtr.py, dbt test runner. Bump hologram version. 2020-11-13 14:34:10 -05:00
Gerda Shank
13b099fbd0 Merge pull request #2883 from fishtown-analytics/feature/2824-parse-only-command
Add parse command and collect parse timing info [#2824]
2020-11-13 10:19:19 -05:00
Gerda Shank
1a8416c297 Add parse command and collect parse timing info [#2824] 2020-11-12 13:56:41 -05:00
Maximilian Roos
8538bec99e _ 2020-11-11 13:48:41 -08:00
Maximilian Roos
f983900597 google-cloud-bigquery goes to 3 2020-11-10 23:51:15 -08:00
Gerda Shank
3af02020ff Merge pull request #2866 from fishtown-analytics/feature/2693-selectors-to-manifest
Save selector dictionary and write out in manifest [#2693][#2800]
2020-11-10 11:48:19 -05:00
Maximilian Roos
8c71488757 _ 2020-11-10 08:38:43 -08:00
Gerda Shank
74316bf702 Save selector dictionary and write out in manifest [#2693][#2800] 2020-11-10 11:17:14 -05:00
Maximilian Roos
7aa8c435c9 Bump protobuf too 2020-11-09 17:36:41 -08:00
Maximilian Roos
daeb51253d Unpin google-cloud dependencies 2020-11-09 17:18:42 -08:00
Jeremy Cohen
0ce2f41db4 Reorg #2837 in changelog 2020-11-09 09:46:08 -05:00
Jeremy Cohen
02e5a962d7 Merge pull request #2837 from franloza/feature/2647-relation-name-in-metadata
Store relation name in manifest's node and source objects
2020-11-09 09:44:40 -05:00
Jeremy Cohen
dcc32dc69f Merge pull request #2850 from elexisvenator/patch-1
Postgres: Prevent temp relation identifiers from being too long
2020-11-09 09:32:35 -05:00
Gerda Shank
af3d6681dd extend timeout for test/rpc 2020-11-06 17:45:35 -05:00
Gerda Shank
106968a3be Merge pull request #2858 from fishtown-analytics/dependabot/pip/docker/requirements/cryptography-3.2
Bump cryptography from 2.9.2 to 3.2 in /docker/requirements
2020-11-06 15:54:49 -05:00
Ben Edwards
2cd56ca044 Update changelog 2020-11-03 20:58:01 +11:00
Ben Edwards
eff198d079 Add integration tests 2020-11-03 20:56:02 +11:00
Ben Edwards
c3b5b88cd2 Postgres: Prevent temp relation identifiers from being too long
Related: #2197 

The currently postgres `make_temp_relation` adds a 29 character suffix to the end of the temp relation identifier (9 from default suffix and 20 from timestamp).  This is a problem now that relations with more than 63 characters raise exceptions. 
The fix is to shorten the suffix and also trim the base_relation identifier so that the total length is always less than 63 characters.

An exception can also be raised if the default suffix is overridden with a value that is too long.
2020-11-03 20:56:02 +11:00
Kyle Wigley
4e19e87bbc Merge pull request #2859 from fishtown-analytics/fix/update-test-container
add unixodbc-dev to testing docker image
2020-10-30 09:56:39 -04:00
Kyle Wigley
6be6f6585d update changelog 2020-10-29 16:52:09 -04:00
Kyle Wigley
d7579f0c99 add g++ and unixodbc-dev 2020-10-29 16:22:46 -04:00
Fran Lozano
b741679c9c Add missing key to child map in expected_bigquery_complex_manifest 2020-10-29 17:25:18 +01:00
Fran Lozano
852990e967 Fix child_map in tests 2020-10-28 22:18:32 +01:00
Fran Lozano
21fd75b500 Fix parent_map object in tests 2020-10-28 19:59:36 +01:00
Fran Lozano
3e5d9010a3 Add snapshot to additional Redshift and Bigquery manifest tests 2020-10-28 19:39:04 +01:00
Fran Lozano
784616ec29 Add relation name to source object in manifest 2020-10-28 18:58:25 +01:00
Fran Lozano
6251d19946 Use is_ephemeral_model property instead of config.materialized
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-10-28 09:49:44 +01:00
dependabot[bot]
17b1332a2a Bump cryptography from 2.9.2 to 3.2 in /docker/requirements
Bumps [cryptography](https://github.com/pyca/cryptography) from 2.9.2 to 3.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/2.9.2...3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-27 22:22:14 +00:00
Jeremy Cohen
74eec3bdbe Merge pull request #2855 from brangisom/brangisom/spectrum-filter-fix
Fix the filtering for external tables in the Redshift get_columns_in_relation macro
2020-10-27 15:03:59 -04:00
Fran Lozano
a9901c4ea7 Disable snapshot documentation testing for Redshift and Bigquery 2020-10-27 19:27:54 +01:00
Brandon Isom
348a2f91ee Move a CHANGELOG entry 2020-10-27 11:17:13 -07:00
Fran Lozano
7115d862ea Modify snapshot path for docs generation tests 2020-10-27 18:59:32 +01:00
Fran Lozano
52ed4aa631 Fix tests which are missing snapshot nodes 2020-10-27 18:45:00 +01:00
Fran Lozano
92cedf8931 Fix Flake8 style issue 2020-10-27 17:39:44 +01:00
Fran Lozano
e1097f11b5 Define relation_name only for non-ephemeral models, seeds and snapshots 2020-10-27 17:23:23 +01:00
Brandon Isom
eb34c0e46b Add stuff to changelog per checklist 2020-10-26 20:17:03 -07:00
Brandon Isom
ee2181b371 Merge branch 'brangisom/spectrum-filter-fix' of github.com:brangisom/dbt into brangisom/spectrum-filter-fix 2020-10-26 19:44:45 -07:00
Brandon Isom
2a5d090e91 Pushes the table_schema = '{{ relation.schema }}' filter into the svv_external_columns CTE 2020-10-26 19:38:33 -07:00
Brandon Isom
857bebe819 Pushes the table_schema = '{{ relation.schema }}' clause down into the svv_external_columns CTE. 2020-10-26 18:29:47 -07:00
Jeremy Cohen
9728152768 Merge pull request #2851 from hochoy/add-python-regex
Folllow up: Support for python "re" module for doing regex in jinja templates
2020-10-26 16:27:17 -04:00
Wai Ho Choy
2566a85429 edit CHANGELOG.md 2020-10-26 12:21:30 -07:00
Wai Ho Choy
46b3130198 lint 2020-10-25 21:18:23 -07:00
Wai Ho Choy
8664516c8d fix blank line linting 2020-10-25 12:03:10 -07:00
Wai Ho Choy
0733c246ea add all exports from python re module 2020-10-25 11:31:33 -07:00
Fran Lozano
4203985e3e Adapt expected_seeded_manifest method to Snowflake identifier quoting 2020-10-25 18:50:52 +01:00
Fran Lozano
900298bce7 Fix database name in relation_name in expected_run_results 2020-10-25 18:06:18 +01:00
Fran Lozano
09c37f508e Adapt relation_name to expected_run_results parameters 2020-10-25 17:27:46 +01:00
Fran Lozano
c9e01bcc81 Fix quotes in relation name for Bigquery docs generate tests 2020-10-25 16:31:00 +01:00
Fran Lozano
b079545e0f Adapt relation_name for Bigquery and Snowflake in docs generation tests 2020-10-25 15:58:04 +01:00
Fran Lozano
c3bf0f8cbf Add relation_name to missing tests in test_docs_generate 2020-10-25 14:11:33 +01:00
Jeremy Cohen
e945bca1d9 Merge pull request #2596 from ran-eh/re-partition-metadata
Make partition metadata available to BigQuery users
2020-10-22 20:58:17 -04:00
Jeremy Cohen
bf5835de5e Merge branch 'dev/kiyoshi-kuromiya' into re-partition-metadata 2020-10-22 20:18:31 -04:00
Ran Ever-Hadani
7503f0cb10 merge from dev/kiyoshi-kuromiya 2020-10-22 16:23:02 -07:00
Ran Ever-Hadani
3a751bcf9b Update CHANGELOG.md 2020-10-22 15:53:25 -07:00
Jeremy Cohen
c31ba101d6 Add tests for get_partitions_metadata (#1)
* Add tests using get_partitions_metadata

* Readd asterisk to raw_execute
2020-10-21 16:00:10 -07:00
Jeremy Cohen
ecadc74d44 Merge pull request #2841 from feluelle/dev/kiyoshi-kuromiya
Respect --project-dir in dbt clean command
2020-10-21 16:01:11 -04:00
Jeremy Cohen
63d25aaf19 Update changelog to account for v0.19.0-b1 release 2020-10-21 09:47:36 -04:00
feluelle
5af82c3c05 Add test that checks if targets were successfully deleted 2020-10-21 10:27:39 +02:00
feluelle
8b4d74ed17 Add changelog entry for resolving issue #2840 2020-10-21 10:27:39 +02:00
feluelle
6a6a9064d5 Respect --project-dir in dbt clean command 2020-10-21 10:25:17 +02:00
Github Build Bot
b188a9488a Merge remote-tracking branch 'origin/releases/0.19.0b1' into dev/kiyoshi-kuromiya 2020-10-21 00:46:30 +00:00
Github Build Bot
7c2635f65d Release dbt v0.19.0b1 2020-10-21 00:35:44 +00:00
Jeremy Cohen
c67d0a0e1a Readd bumpversion config header 2020-10-20 18:01:04 -04:00
Fran Lozano
7ee78e89c9 Add missing relation_name fields in doc generation test manifests 2020-10-20 19:18:38 +02:00
Fran Lozano
40370e104f Fix wrong schema name in test and add missing relation_name in node 2020-10-20 18:48:59 +02:00
Fran Lozano
a8809baa6c Merge branch 'dev/kiyoshi-kuromiya' into feature/2647-relation-name-in-metadata 2020-10-20 18:32:53 +02:00
Fran Lozano
244d5d2c3b Merge remote-tracking branch 'upstream/dev/kiyoshi-kuromiya' into dev/kiyoshi-kuromiya 2020-10-20 18:26:28 +02:00
Fran Lozano
a0370a6617 Add relation_name to node object in docs generation tests 2020-10-20 18:22:32 +02:00
Jeremy Cohen
eb077fcc75 Merge pull request #2845 from fishtown-analytics/docs/0.19.0-b1
dbt-docs changes for dbt v0.19.0-b1
2020-10-20 09:57:11 -04:00
Jeremy Cohen
c5adc50eed Make flake8 happy 2020-10-19 18:42:23 -04:00
Jeremy Cohen
6e71b6fd31 Include dbt-docs changes for v0.19.0-b1 2020-10-19 18:41:31 -04:00
Gerda Shank
278382589d Merge pull request #2834 from fishtown-analytics/feature/remove_injected_sql
Remove injected_sql. Store non-ephemeral injected_sql in compiled_sql
2020-10-19 18:08:41 -04:00
Gerda Shank
6f0f6cf21a Merge branch 'dev/0.18.1' into dev/kiyoshi-kuromiya 2020-10-19 11:30:52 -04:00
Fran Lozano
01331ed311 Update CHANGELOG.md 2020-10-16 19:08:30 +02:00
Fran Lozano
f638a3d50c Store relation name in manifest's node object 2020-10-16 18:38:22 +02:00
Gerda Shank
512c41dbaf Remove injected_sql. Store non-ephemeral injected_sql in compiled_sql 2020-10-15 11:52:03 -04:00
Github Build Bot
f6bab4adcf Release dbt v0.18.1 2020-10-13 21:31:54 +00:00
Jeremy Cohen
526ecee3da Merge pull request #2832 from fishtown-analytics/fix/colorama-upper-044
Set colorama upper bound to <0.4.4
2020-10-13 17:20:05 -04:00
Jeremy Cohen
1bc9815d53 Set colorama upper bound to <0.4.4 2020-10-13 16:26:10 -04:00
Ran Ever-Hadani
78bd7c9465 Eliminate asterisk from raw_execute to try an fix integration error 2020-10-11 12:06:56 -07:00
Ran Ever-Hadani
d74df8692b Eliminate pep8 errors 2020-10-11 11:37:51 -07:00
Ran Ever-Hadani
eda86412cc Accommodate first round of comments 2020-10-11 11:03:53 -07:00
Ran Ever-Hadani
cce5945fd2 Make partition metadata available to BigQuery users (rebased to dev/kiyoshi-kuromiya) 2020-10-10 17:44:07 -07:00
Drew Banin
72038258ed Merge pull request #2805 from fishtown-analytics/feature/bigquery-oauth-token
Support BigQuery OAuth using a refresh token and client secrets
2020-10-09 14:55:00 -04:00
Drew Banin
056d8fa9ad Merge branch 'dev/kiyoshi-kuromiya' into feature/bigquery-oauth-token 2020-10-09 14:07:27 -04:00
Gerda Shank
3888e0066f Merge pull request #2813 from fishtown-analytics/feature/2510-save-args-run_results
Save args in run_results.json
2020-10-09 13:51:14 -04:00
Drew Banin
ee6571d050 Merge branch 'dev/kiyoshi-kuromiya' into feature/bigquery-oauth-token 2020-10-09 10:15:23 -04:00
Gerda Shank
9472288304 Save args in run_results.json 2020-10-08 17:39:03 -04:00
Jeremy Cohen
fd5e10cfdf Merge pull request #2817 from zmac12/feature/addDebugQueryMethod
Feature/add debug query method
2020-10-08 10:30:31 -04:00
Jeremy Cohen
aeae18ec37 Merge pull request #2821 from joelluijmes/feature/hard-delete-revival
Re-instate hard-deleted records during snapshot
2020-10-08 10:27:42 -04:00
Zach McQuiston
03d3943e99 fixing linter problem in connections.py 2020-10-08 07:47:16 -06:00
Zach McQuiston
214d137672 adding entry to changelog 2020-10-08 07:45:35 -06:00
Joël Luijmes
83db275ddf Added changelog entry 2020-10-08 15:26:51 +02:00
Joël Luijmes
b8f16d081a Remove redundant 'dbt_valid_to is null' checks 2020-10-08 15:24:04 +02:00
Joël Luijmes
675b01ed48 Re-snapshot records that were invalidated through hard-delete 2020-10-08 12:45:35 +02:00
Joël Luijmes
b20224a096 Refactor test for hard-delete snapshots 2020-10-08 12:45:35 +02:00
Zach McQuiston
fd6edfccc4 Removing accidental whitespace addition 2020-10-07 19:30:48 -06:00
Zach McQuiston
4c58438e8a removing errant reference to debug_query method 2020-10-07 19:29:49 -06:00
Zach McQuiston
5ff383a025 Adding type hint for debug_query 2020-10-07 19:25:00 -06:00
Zach McQuiston
dcb6854683 adding debug_query to base/impl.py enabling plugin authors to write their own debug_query 2020-10-07 19:20:16 -06:00
Drew Banin
e4644bfe5a support providing a token directly; update method name 2020-10-07 15:34:57 -04:00
Jeremy Cohen
93168fef87 Merge pull request #2809 from mescanne/bigquery_invocation_id
Add invocation_id to BigQuery jobs
2020-10-07 10:41:39 -04:00
Jeremy Cohen
9832822bdf Merge branch 'dev/kiyoshi-kuromiya' into bigquery_invocation_id 2020-10-07 09:45:21 -04:00
Mark Scannell
5d91aa3bcd Updated feature request resolution. 2020-10-07 10:55:35 +01:00
Zach McQuiston
354ab5229b adding new debug_query function to base debug task 2020-10-03 16:20:52 -06:00
Zach McQuiston
00de0cd4b5 adding new debug_query function to base adapter 2020-10-03 16:19:48 -06:00
Mark Scannell
26210216da Only set invocation_id if tracking is enabled 2020-10-03 15:32:53 +01:00
Mark Scannell
e29c14a22b updated changelog 2020-10-03 13:09:04 +01:00
Mark Scannell
a6990c8fb8 fixe up 2020-10-03 13:04:56 +01:00
Mark Scannell
3e40e71b96 Added dbt_invocation_id to BigQuery jobs 2020-10-03 13:01:25 +01:00
Gerda Shank
3f45abe331 Merge pull request #2799 from fishtown-analytics/feature/2765-save-manifest
write manifest when writing run_results
2020-10-01 16:12:15 -04:00
Gerda Shank
6777c62789 Merge pull request #2804 from fishtown-analytics/rpc-test-timeouts
Increase rpc test timouts to avoid local test failures
2020-10-01 16:10:59 -04:00
Github Build Bot
1aac869738 Merge remote-tracking branch 'origin/releases/0.18.1rc1' into dev/0.18.1 2020-10-01 16:52:51 +00:00
Github Build Bot
493554ea30 Release dbt v0.18.1rc1 2020-10-01 16:39:50 +00:00
Drew Banin
1cf87c639b (#2344) Support BigQuery OAuth using a refesh token and client secrets 2020-09-30 23:04:40 -04:00
Gerda Shank
2cb3d92163 Increase rpc test timouts to avoid local test failures 2020-09-30 17:34:23 -04:00
Jeremy Cohen
89b6e52a73 Merge pull request #2791 from kingfink/tf/fix-snapshot-compilation-error
Fix snapshot compilation error
2020-09-30 16:54:45 -04:00
Tim Finkel
97407c10ff revert dockerfile 2020-09-30 15:54:50 -04:00
Tim Finkel
81222dadbc Merge branch 'tf/fix-snapshot-compilation-error' of https://github.com/kingfink/dbt into tf/fix-snapshot-compilation-error 2020-09-30 15:53:40 -04:00
Tim Finkel
400555c391 update tests 2020-09-30 15:52:12 -04:00
Tim Finkel
9125b05809 Update CHANGELOG.md 2020-09-30 14:46:45 -04:00
Jeremy Cohen
139b353a28 Merge pull request #2796 from Foxtel-DnA/feature/6434-bq-retry-rate-limit
UPDATE _is_retryable() to handle BQ rateLimitExceeded
2020-09-30 13:09:11 -04:00
Jared Champion (SYD)
fc474a07d0 REBASED on dev/0.18.1; moved CHANGELOG entries 2020-09-30 10:57:03 +10:00
championj-foxtel
8fd8fa09a5 Merge pull request #1 from fishtown-analytics/dev/0.18.1
Dev/0.18.1
2020-09-30 09:56:27 +10:00
Gerda Shank
41ae831d0e write manifest when writing run_results 2020-09-29 16:35:44 -04:00
Gerda Shank
dbca540d70 Merge pull request #2781 from fishtown-analytics/feature/2700-improve-yaml-selector-errors
Add better error messages for yaml selectors
2020-09-29 16:16:44 -04:00
Tim Finkel
dc7eca4bf9 update changelog 2020-09-25 17:07:05 -04:00
Tim Finkel
fb07149cb7 fix snapshot compliation error 2020-09-25 16:52:14 -04:00
Github Build Bot
b2bd5a5548 Merge remote-tracking branch 'origin/releases/0.18.1b3' into dev/0.18.1 2020-09-25 20:20:21 +00:00
Github Build Bot
aa6b333e79 Release dbt v0.18.1b3 2020-09-25 20:05:31 +00:00
Jeremy Cohen
0cb9740535 Merge pull request #2789 from fishtown-analytics/fix/require-keyring
Fix: require keyring on snowflake
2020-09-25 15:00:12 -04:00
Gerda Shank
46eadd54e5 Add better error messages for yaml selectors 2020-09-25 14:53:30 -04:00
Jeremy Cohen
6b032b49fe Merge branch 'dev/0.18.1' into fix/require-keyring 2020-09-25 14:13:53 -04:00
Jeremy Cohen
35f78ee0f9 Merge pull request #2754 from aiguofer/include_external_tables_in_get_columns_in_relation
Include external tables in get_columns_in_relation redshift adapter
2020-09-25 13:25:06 -04:00
Jeremy Cohen
5ec36df7f0 Merge branch 'dev/0.18.1' into include_external_tables_in_get_columns_in_relation 2020-09-25 12:52:39 -04:00
Jeremy Cohen
f918fd65b6 Merge pull request #2766 from jweibel22/fix/redshift-iam-concurrency-issue
Give each redshift client their own boto session
2020-09-25 12:50:20 -04:00
Jeremy Cohen
d08a39483d PR feedback 2020-09-25 12:11:49 -04:00
Jeremy Cohen
9191f4ff2d Merge branch 'dev/0.18.1' into fix/redshift-iam-concurrency-issue 2020-09-25 12:10:33 -04:00
Jeremy Cohen
19232f554f Merge pull request #2785 from fishtown-analytics/feature/metadata-env-vars
add env vars with a magic prefix to the metadata
2020-09-24 17:03:30 -04:00
Jeremy Cohen
b4a83414ac Require optional dep (keyring) on snowflake 2020-09-24 15:49:35 -04:00
Jeremy Cohen
cb0e62576d Merge pull request #2732 from Mr-Nobody99/feature/add-snowflake-last-modified
Added last_altered query to Snowflake catalog macro
2020-09-24 15:29:04 -04:00
Alexander Kutz
e3f557406f Updated test/integration/029_docs_generate_test.py to reflect new stat 2020-09-23 11:08:08 -05:00
Jacob Beck
676af831c0 add env vars with a magic prefix to the metadata 2020-09-23 10:04:06 -06:00
Jacob Beck
873d76d72c Merge pull request #2786 from fishtown-analytics/feature/invocation-id
add invocation_id to artifact metadata
2020-09-23 10:03:42 -06:00
Jacob Beck
8ee490b881 Merge pull request #2749 from joelluijmes/snapshot-hard-deletes-joell
Include hard-deletes when making snapshot
2020-09-23 08:31:48 -06:00
Jacob Beck
ff31b277f6 add invocation_id to artifact metadata 2020-09-23 08:07:20 -06:00
Jacob Beck
120eb5b502 Merge pull request #2778 from fishtown-analytics/feature/common-artifact-metadata
Feature: common artifact metadata
2020-09-23 08:06:45 -06:00
Alexander Kutz
a93e288d6a Added missing commaabove addition. 2020-09-22 17:48:48 -05:00
Alexander Kutz
8cf9311ced Changed 'BASE_TABLE' to 'BASE TABLE' 2020-09-22 16:04:30 -05:00
Alexander Kutz
713e781473 Reset branch against dev/0.8.1 and re-added changes.
udpated changelog.md
2020-09-22 14:54:56 -05:00
Jacob Beck
a32295e74a fix schema collection script 2020-09-22 13:48:31 -06:00
Jacob Beck
204b02de3e fix freshness RPC response behavior 2020-09-22 12:49:17 -06:00
Jacob Beck
8379edce99 Add a common metadata field to JSON artifacts
Adjusted how schema versions are set
RPC calls no longer have the schema version in their replies
2020-09-22 10:35:02 -06:00
Github Build Bot
e265ab67c7 Merge remote-tracking branch 'origin/releases/0.18.1b2' into dev/0.18.1 2020-09-22 14:23:45 +00:00
Github Build Bot
fde1f13b4e Release dbt v0.18.1b2 2020-09-22 14:09:51 +00:00
Jeremy Cohen
9c3839c7e2 Merge pull request #2782 from fishtown-analytics/docs/0.18.1-exposures
[revised] dbt-docs changes for v0.18.1
2020-09-22 09:51:04 -04:00
Jeremy Cohen
c0fd702cc7 Rename reports --> exposures 2020-09-22 08:44:58 -04:00
Jacob Beck
429419c4af Merge pull request #2780 from fishtown-analytics/feature/rename-results-to-exposures
reports -> exposures
2020-09-21 15:15:11 -06:00
Jacob Beck
56ae20602d reports -> exposures 2020-09-21 14:46:48 -06:00
Jacob Beck
a4b80cc2e4 Merge branch 'dev/kiyoshi-kuromiya' into snapshot-hard-deletes-joell 2020-09-21 14:11:01 -06:00
Jacob Beck
4994cc07a0 Merge pull request #2767 from fishtown-analytics/feature/schema-versions
Feature/schema versions
2020-09-21 14:10:28 -06:00
Joël Luijmes
e96cf02561 Merge remote-tracking branch 'upstream/dev/kiyoshi-kuromiya' into snapshot-hard-deletes-joell 2020-09-21 21:43:49 +02:00
Jacob Beck
764c9b2986 PR feedback 2020-09-21 11:56:07 -06:00
jweibel22
40c6499d3a Update CHANGELOG.md
Co-authored-by: Jacob Beck <beckjake@users.noreply.github.com>
2020-09-20 13:31:43 +02:00
Jimmy Rasmussen
3a78efd83c Add test cases to ensure default boto session is not used 2020-09-20 13:31:15 +02:00
Jimmy Rasmussen
eb33cf75e3 Add entry to CHANGELOG 2020-09-18 10:44:00 +02:00
Jimmy Rasmussen
863d8e6405 Give each redshift client their own boto session
since the boto session is not thread-safe, using the default session in a multi-threaded scenario will result in concurrency errors
2020-09-18 10:29:26 +02:00
Jacob Beck
1fc5a45b9e Merge pull request #2768 from fishtown-analytics/fix/makefile-on-macos
fix the new makefile on macos
2020-09-17 13:42:32 -06:00
Github Build Bot
7751fece35 Merge remote-tracking branch 'origin/releases/0.18.1b1' into dev/0.18.1 2020-09-17 19:09:23 +00:00
Github Build Bot
7670c42462 Release dbt v0.18.1b1 2020-09-17 18:54:44 +00:00
Jacob Beck
b72fc3cd25 fix the new makefile on macos 2020-09-17 11:47:06 -06:00
Jacob Beck
4cc1a4f74c changelog 2020-09-17 10:04:28 -06:00
Jacob Beck
540607086c fix docker message 2020-09-17 10:03:32 -06:00
Jacob Beck
7d929e98af Embed schema/dbt versions into the json schema for artifacts 2020-09-17 10:03:32 -06:00
Joël Luijmes
0086097639 Fix non-deterministic behavior by sorting on id (redshift test failed) 2020-09-17 11:55:23 +02:00
Jacob Beck
daff0badc8 Merge branch 'dev/0.18.1' into dev/kiyoshi-kuromiya 2020-09-16 14:16:33 -06:00
Jacob Beck
22c4d8fabe Merge pull request #2741 from heisencoder/fix/docker-testing-on-linux
Fix docker-based testing for Linux users
2020-09-16 13:39:02 -06:00
Jeremy Cohen
3485482460 Merge pull request #2760 from fishtown-analytics/docs/0.18.1-reports
dbt-docs changes for v0.18.1
2020-09-16 15:21:54 -04:00
Jacob Beck
c43873379c Merge pull request #2758 from fishtown-analytics/fix/version-bump
Bump version: 0.18.0 → 0.19.0a1
2020-09-16 13:17:58 -06:00
Jeremy Cohen
ea5e5df5a3 Support reports in dbt-docs 2020-09-16 13:44:17 -04:00
Jacob Beck
f2caf2f1ff Merge pull request #2752 from fishtown-analytics/feature/reports
Feature: reports
2020-09-16 10:41:43 -06:00
Jacob Beck
07d4020fca Bump version: 0.18.0 → 0.19.0a1 2020-09-16 10:34:53 -06:00
Jacob Beck
2142e529ff PR feedback: make report selector more like source selector, remove reports from fqn slector
Make some corresponding fqn adjustments
Add dbt ls report output
Fix dbt ls source output
The default selector now also returns reports
Update tests
2020-09-16 07:23:36 -06:00
Joël Luijmes
b9d502e2e6 Ensure dbt_valid_to is latest column 2020-09-16 09:11:42 +02:00
Joël Luijmes
8c80862c10 Snapshot hard-delete tests also for bigquery, snowflake and redshift 2020-09-16 08:41:45 +02:00
Joël Luijmes
2356c7b63d Use dict.get for optional paramater invalidate_hard_deletes 2020-09-16 07:51:01 +02:00
Diego Fernandez
9c24fc25f5 Add entry to CHANGELOG 2020-09-15 15:11:05 -06:00
Diego Fernandez
4f1a6d56c1 Include external tables in get_columns_in_relation redshift adapter 2020-09-15 15:09:55 -06:00
Joël Luijmes
b71b7e209e Update changelog 2020-09-15 22:01:02 +02:00
Joël Luijmes
2581e98aff Snapshot hard-delete opt-in during config 2020-09-15 16:47:16 +02:00
Joël Luijmes
afc7136bae Fix rpc integration snapshot tests 2020-09-15 16:47:11 +02:00
Joël Luijmes
e489170558 Add test for snapshotting hard deleted records 2020-09-15 16:45:36 +02:00
Joël Luijmes
50106f2bd3 Include hard-deletes when making snapshot
It sets dbt_valid_to to the current snapshot time.
2020-09-15 16:45:36 +02:00
Jacob Beck
e96f4a5be6 got redshifted 2020-09-14 14:35:00 -06:00
Jacob Beck
4768ac5fda Fix and add new tests, update changelog 2020-09-14 14:35:00 -06:00
Jacob Beck
c91fcc527a add comparison/selector logic for reports 2020-09-14 12:24:00 -06:00
Jacob Beck
8520ff35b3 Add reports feature
Add ParsedReport/UnparsedReport
add report parser and report node logic to manifest/results/dbt ls/selectors
NonSourceNode -> ManifestNode
add GraphMemberNode type that includes reports
2020-09-14 11:36:31 -06:00
Jacob Beck
9b8a98f4ec Test quality of life/cleanup
remove unused test folders
move rpc tests from 048 to 100 for convenience
 - Migrating these to the test/rpc model is going to take work. In the
   interim, developers can now use `tests/integeration/0*` to run all non-rpc
   tests.
2020-09-14 10:26:02 -06:00
Jacob Beck
4bd4afaec7 bumpversion for 0.18.1 2020-09-11 14:21:03 -06:00
Matt Ball
69352d8414 Fix docker-based testing for Linux users
See https://github.com/fishtown-analytics/dbt/issues/2739

This change enables Linux users to run the dbt tests via the docker
image. It follows the recommendations from this article:
https://jtreminio.com/blog/running-docker-containers-as-current-host-user/

Notable changes:
*  Added new Makefile rule to generate a .env file that contains USER_ID
and GROUP_ID environment variables to the ID of the current user. This
is in turn used by docker-compose and the Dockerfile to make the Docker
image run as the current user. (Note that on Windows or Mac, this
behavior happens by default).
*  Reordered Dockerfile to allow for better caching of intermediate
images (i.e., put things that don't depend on ARGS earlier).
*  Bumped CircleCI's Dockerfile from version 7 to 9.  Jake rebuilt
9 off of this PR.
2020-09-11 11:15:18 -06:00
Jeremy Cohen
4a21ea6575 Merge pull request #2723 from tpilewicz/fix/freshness-logs
Feat(result logs): Use three logging levels
2020-09-09 15:03:06 -04:00
Thomas Pilewicz
86bbb9fe38 Add contributors section to 0.18.1 2020-09-09 18:50:39 +02:00
Thomas Pilewicz
4030d4fc20 Move logging levels changelog entry to 0.18.1 2020-09-09 18:50:27 +02:00
tpilewicz
182f69a9ec Merge pull request #2 from fishtown-analytics/dev/0.18.1 2020-09-09 18:43:45 +02:00
Gerda Shank
fb40efe4b7 Merge pull request #2733 from fishtown-analytics/fix/2539-comment-quoting
When column config says quote, use quotes in SQL to add comments
2020-09-08 14:40:27 -04:00
Jacob Beck
9d00c00072 Merge pull request #2735 from fishtown-analytics/feature/include-unrendered-configs-2
Feature: include unrendered configs
2020-09-08 10:57:29 -06:00
Jacob Beck
10c3118f9c Merge branch 'dev/kiyoshi-kuromiya' into feature/include-unrendered-configs-2 2020-09-08 09:29:09 -06:00
Jacob Beck
1fa149dca2 Merge pull request #2736 from fishtown-analytics/feature/rpc-state-defer
Feature: state and defer in RPC calls
2020-09-08 09:28:02 -06:00
Jacob Beck
60f4c963b5 Merge branch 'dev/kiyoshi-kuromiya' into feature/rpc-state-defer 2020-09-08 08:41:21 -06:00
Gerda Shank
51b8e64972 When column config says quote, use quotes in SQL to add comments
Add separate test for column comments. Fix Snowflake catalog comments.
2020-09-04 16:44:19 -04:00
Jacob Beck
ae542dce74 Merge dev/marian-anderson 2020-09-04 07:12:04 -06:00
Github Build Bot
fa8a4f2020 Merge remote-tracking branch 'origin/releases/0.18.0' into dev/marian-anderson 2020-09-03 16:45:49 +00:00
Github Build Bot
481bdd56d3 Release dbt v0.18.0 2020-09-03 16:02:36 +00:00
Github Build Bot
1a9083ddb7 Merge remote-tracking branch 'origin/releases/0.18.0rc2' into dev/marian-anderson 2020-09-03 15:52:16 +00:00
Github Build Bot
9779f43620 Release dbt v0.18.0rc2 2020-09-03 15:49:09 +00:00
Jacob Beck
d31e82edfc this does not belong here 2020-09-02 10:13:41 -06:00
Jeremy Cohen
981535a1c3 Merge pull request #2734 from fishtown-analytics/docs/0.18.0-followup
dbt-docs changes for v0.18.0 (final)
2020-09-01 17:14:16 -04:00
Jacob Beck
5354e39e5f add defer/state args to RPC, add tests 2020-09-01 14:25:28 -06:00
Jacob Beck
ca9293cbfb changelog 2020-09-01 14:22:58 -06:00
Jeremy Cohen
e2fe6a8249 Add project-level overviews 2020-09-01 14:58:15 -04:00
Jeremy Cohen
a8347b7ada Add missing changelog entry 2020-09-01 14:57:47 -04:00
Jacob Beck
bcbf7c3b7b remove default values from unrendered configs 2020-09-01 10:30:03 -06:00
Jacob Beck
6a26cb280f fix the tests, add unrendered configs for sources 2020-09-01 10:30:03 -06:00
Jacob Beck
fd658ace9d Attach unrendered configs to parsed nodes 2020-09-01 10:30:03 -06:00
Jacob Beck
5e71a2aa3f Add unrendered configs to project 2020-09-01 10:30:03 -06:00
Jacob Beck
e3fb923b34 removed v1 config 2020-09-01 10:30:03 -06:00
Jeremy Cohen
f55b257609 Merge pull request #2722 from genos/genos/fix-for-2347
fix for 2347
2020-09-01 08:50:04 -04:00
genos
81bf3dae5c add contributors to changelog 2020-08-31 23:48:49 -04:00
Graham
d0074f3411 Merge branch 'dev/marian-anderson' into genos/fix-for-2347 2020-08-31 09:30:52 -04:00
Gerda Shank
2cc2d971c6 Merge pull request #2727 from fishtown-analytics/fix/2197-long-table-names
Check Postgres relation name lengths and throw error when over 63
2020-08-28 09:31:23 -04:00
Gerda Shank
5830f5590e Tweak error message, reformat for flake8 2020-08-27 16:35:35 -04:00
Jacob Beck
75facebe80 Merge pull request #2726 from fishtown-analytics/fix/require-version-validation
Validate require-dbt-version before validating dbt_project.ymls chema
2020-08-27 10:01:13 -06:00
Jacob Beck
0130398e9f Update core/dbt/config/project.py
Co-authored-by: Kyle Wigley <kyle@fishtownanalytics.com>
2020-08-27 08:16:33 -06:00
Gerda Shank
22d9b86e9f update changelog for #2197 2020-08-26 14:15:01 -04:00
Gerda Shank
c87b671275 Use csv for data in test 063; tweak several lines 2020-08-26 13:57:03 -04:00
Jacob Beck
1eb5857811 add missing unit tests 2020-08-26 08:40:53 -06:00
Gerda Shank
5fc1cb39a6 Check Postgres relation name lengths and throw error when over 63 2020-08-26 10:28:37 -04:00
Jacob Beck
1f8e29276e move the require-dbt-version check to before parsing
update changelog
2020-08-25 13:17:00 -06:00
Thomas Pilewicz
cf02c7fd02 Fix(get_printable_result): return type hints 2020-08-25 16:52:21 +02:00
Thomas Pilewicz
5d93c64c0e Add logging levels to features of 0.18.0 2020-08-24 15:59:31 +02:00
Thomas Pilewicz
c738928ea3 Feat(result logs): Use three logging levels 2020-08-24 14:04:05 +02:00
genos
707310db64 update changelog 2020-08-23 17:40:36 -04:00
genos
59bf43dc1f Fix for #2347
**Introduction**

This PR attempts to fix #2347, wherein we wish `dbt` to complain about trying to install with a Python version < 3.6.

**Changes**

Per [the issue's suggestion](https://github.com/fishtown-analytics/dbt/issues/2347), I found every `setup.py` file I could:

```
-# If you have the fantastic `fd` utility installed:
fd setup.py
-# This also works
find . -name setup.py -print
```

Then to each of these, I added the following after the `import sys`:

```
if sys.version_info < (3, 6):
    print('Error: dbt does not support this version of Python.')
    print('Please upgrade to Python 3.6 or higher.')
    sys.exit(1)
```

**Testing**

I used the [`nix` package manager](https://nixos.org) to attempt installing this branch with both Python 2.7 and Python 3.8.

_Python 2.7_ fails as expected:

```
~/github/test2 ∃ cat default.nix
let
  pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/20.03.tar.gz") { };
  py = pkgs.python27Full.withPackages (p: [ p.setuptools ]);
in pkgs.mkShell {
  name = "python-2-env";
  buildInputs = [ py ];
}
~/github/test2 ∃ nix-shell --pure

[nix-shell:~/github/test2]$ python ../dbt/setup.py build
Error: dbt does not support this version of Python.
Please upgrade to Python 3.6 or higher.

[nix-shell:~/github/test2]$ echo $?
1
```

_Python 3.8_ still works:

```
~/github/test3 ∃ cat default.nix
let
  pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/20.03.tar.gz") { };
  py = pkgs.python38Full.withPackages (p: [ p.setuptools ]);
in pkgs.mkShell {
  name = "python-3-env";
  buildInputs = [ py ];
}
~/github/test3 ∃ nix-shell --pure

[nix-shell:~/github/test3]$ python ../dbt/setup.py build
running build

[nix-shell:~/github/test3]$ echo $?
0
```
2020-08-23 17:13:36 -04:00
Jacob Beck
fe461381a2 Merge pull request #2721 from fishtown-analytics/feature/more-test-only-adapter-methods
add more test helper methods
2020-08-21 13:13:28 -06:00
Jacob Beck
685873ab42 its ok to hit a deprecation with tracking disabled 2020-08-21 12:31:35 -06:00
Jacob Beck
b6a951903e add more test helper methods 2020-08-21 08:02:45 -06:00
Github Build Bot
acfa84918e Release dbt v0.18.0rc1 2020-08-19 20:10:33 +00:00
Jacob Beck
75304eb3be Merge pull request #2718 from fishtown-analytics/fix/only-jinja-is-comments
When deciding if we should bypass rendering, also check for comments
2020-08-19 13:39:03 -06:00
Jacob Beck
1d7eb59ff2 Merge pull request #2712 from fishtown-analytics/feature/adapter-cte-generation
Feature: adapter cte generation
2020-08-19 12:40:04 -06:00
Jacob Beck
4273cc9e29 Merge pull request #2709 from kconvey/kconvey-copy-job
Add a BigQuery adapter macro to enable usage of CopyJobs
2020-08-19 12:39:49 -06:00
Jacob Beck
29be2de5cb When deciding if we should bypass rendering, also check for comments 2020-08-19 12:27:40 -06:00
Jacob Beck
91b0496c89 Merge branch 'dev/marian-anderson' into kconvey-copy-job 2020-08-19 12:12:14 -06:00
Jacob Beck
f043f948de Merge pull request #2711 from kconvey/kconvey-model-ttl
Support TTL for BigQuery tables
2020-08-19 12:10:14 -06:00
Jeremy Cohen
7ef7a8f306 Merge pull request #2708 from rsenseman/enhancement/color_output_command_line_flag_second_attempt
Add --use-colors cli option (second attempt)
2020-08-19 13:42:24 -04:00
Robert
db8eea2468 Merge branch 'dev/marian-anderson' into enhancement/color_output_command_line_flag_second_attempt 2020-08-19 10:01:12 -07:00
Kurt Convey
55813d9209 hours_to_expiration 2020-08-19 10:08:52 -06:00
Kurt Convey
4c05daae1b Merge with origin 2020-08-19 09:41:37 -06:00
Kurt Convey
16eb7232c3 Check the copy model for failure 2020-08-19 09:18:33 -06:00
Kurt Convey
c28ffcdd9f merge with origin 2020-08-19 09:08:07 -06:00
Kurt Convey
4b8652f1c4 Should be two results for original table and (failing) copy 2020-08-19 08:58:39 -06:00
Jeremy Cohen
674bd8f264 Merge pull request #2715 from fishtown-analytics/docs/0.18.0-search-selectors
Docs site updates for 0.18.0
2020-08-19 10:41:48 -04:00
Jeremy Cohen
1ba832dbfe Docs site updates for 0.18.0 2020-08-18 18:42:32 -04:00
Jacob Beck
d3e4d3fbcb pr feedback: remove commented out code 2020-08-18 14:34:12 -06:00
Jacob Beck
58a3cb4fbd changelog update 2020-08-18 14:34:11 -06:00
Jacob Beck
8ad1551b15 when you think about it, data tests are really just ctes 2020-08-18 14:33:56 -06:00
Jacob Beck
123771163a hide more things from the context 2020-08-18 14:33:56 -06:00
Jacob Beck
f80a759488 Have the adapter be responsible for producing the compiler
The adapter's Relation is consulted for adding the ephemeral model prefix

Also hide some things from Jinja

Have the adapter be responsible for producing the compiler, move CTE generation into the Relation object
2020-08-18 14:33:56 -06:00
Kurt Convey
42f8a4715e Assert that single result has error 2020-08-18 14:11:28 -06:00
Jeremy Cohen
c29892e340 Merge pull request #2710 from fishtown-analytics/feature/add-deprecation-tracking
Track deprecation warnings
2020-08-18 16:11:02 -04:00
Kurt Convey
b4a2ed6bb5 Look for proper string 2020-08-18 14:06:53 -06:00
Kurt Convey
67e8caf045 No need to assert model success 2020-08-18 14:05:44 -06:00
Kurt Convey
2562debe31 Put --debug before run 2020-08-18 13:42:58 -06:00
Kurt Convey
87e2fd610c Raise error for bad materializations and set a default 2020-08-18 13:41:00 -06:00
Kurt Convey
af118bcc53 Check stdout with --debug for actual ddl 2020-08-18 13:23:28 -06:00
Kurt Convey
7e01172b4c Set status 2020-08-18 13:08:58 -06:00
Kurt Convey
f56ae93772 Return from copy_bq_table and fix test 2020-08-18 10:46:04 -06:00
Kurt Convey
3834805929 Use injected sql from results 2020-08-18 10:25:11 -06:00
Kurt Convey
50b6057bbf Make copy a proper materialization 2020-08-18 10:17:15 -06:00
Kurt Convey
c0199abacf Split failing and succeeding models 2020-08-18 08:34:41 -06:00
Kurt Convey
77688c74f3 Remove test from wrong PR 2020-08-17 17:20:24 -06:00
Kurt Convey
47ab7419ac Embed profile name 2020-08-17 17:08:38 -06:00
Kurt Convey
dc209f77ec Fix class name 2020-08-17 16:35:29 -06:00
Kurt Convey
76aa8c7df5 Fix class name 2020-08-17 16:32:22 -06:00
Kurt Convey
671a29ff34 plugins 2020-08-17 16:16:46 -06:00
Kurt Convey
eb35794aca Fix table options string 2020-08-17 16:05:44 -06:00
Kurt Convey
a8d6691dee Mock config better 2020-08-17 15:59:04 -06:00
Kurt Convey
955f4ae977 Add entry to CHANGELOG 2020-08-17 15:26:33 -06:00
Kurt Convey
25a869a686 Fix unit test 2020-08-17 15:23:25 -06:00
Kurt Convey
7aa8030b76 Fix test name 2020-08-17 15:12:02 -06:00
Kurt Convey
108d843bba Add newlines 2020-08-17 15:10:51 -06:00
Kurt Convey
099fea8565 Add integration test 2020-08-17 15:04:59 -06:00
Jeremy Cohen
a573a2ada1 Explicitly import dbt.tracking module 2020-08-17 17:00:07 -04:00
Kurt Convey
1468ca8ebc Expect to fail 2020-08-17 14:54:11 -06:00
Jeremy Cohen
274aea9f8f Track deprecation warnings 2020-08-17 16:36:04 -04:00
Kurt Convey
38a99a75ed Use correct models 2020-08-17 14:29:49 -06:00
Kurt Convey
6e06bd0cb4 Add unit test 2020-08-17 14:29:02 -06:00
Kurt Convey
02a793998a Fix macro compilation 2020-08-17 14:10:04 -06:00
Kurt Convey
81ab9469b7 Add time_to_expiration 2020-08-17 14:00:10 -06:00
Kurt Convey
bcc928495d Fix if statement 2020-08-17 13:13:48 -06:00
Kurt Convey
621ae7dbc9 make flake8 happy 2020-08-17 13:03:33 -06:00
Kurt Convey
85f2c03903 Tweak integration test 2020-08-17 12:26:25 -06:00
Kurt Convey
f7fd741d43 Attempt at integration tests 2020-08-17 12:16:20 -06:00
Kurt Convey
fad0d81837 Reference consts through the right module 2020-08-17 11:56:14 -06:00
Kurt Convey
09687409dc move bq context to impl.py 2020-08-17 11:44:51 -06:00
Kurt Convey
091bcd107c Fix test write dispositions 2020-08-17 09:20:22 -06:00
Kurt Convey
f9b300d63a Add changelog 2020-08-17 09:12:52 -06:00
Jacob Beck
4f2acc2c96 Merge pull request #2703 from vogt4nick/2702-patch-redshift-table-size-estimation
patch redshift table size estimation
2020-08-17 09:04:03 -06:00
Kurt Convey
6aa4a60d5c Add copy_job macro 2020-08-17 09:01:17 -06:00
rsenseman
b075bf51b0 one more changelog update 2020-08-16 14:48:08 -07:00
rsenseman
de2341ece0 update changelog 2020-08-16 14:44:55 -07:00
rsenseman
ff67e7d47c add missing code; finalize pr 2020-08-16 14:34:46 -07:00
rsenseman
31644ed39d initial commit 2020-08-16 13:48:55 -07:00
Jacob Beck
acb235ef4f Merge pull request #2698 from fishtown-analytics/fix/snowflake-connector-python-upgrade
bump requirements, enable the token cache
2020-08-14 13:47:32 -06:00
Jacob Beck
d554835b50 Merge pull request #2695 from fishtown-analytics/feature/state-modified-selector
Add state:modified and state:new selectors
2020-08-14 13:44:48 -06:00
Jacob Beck
c8453d80fc fix ls tests 2020-08-14 11:36:19 -06:00
Jacob Beck
f3f713ae65 fix macro change check to account for new list return value when macros are added/removed 2020-08-14 09:05:55 -06:00
Nick Vogt
cfa741e597 calculate Redshift table size in bytes, not megabytes 2020-08-14 11:02:10 -04:00
Jacob Beck
13fb2351ed PR feedback 2020-08-14 08:48:59 -06:00
Jacob Beck
3555ba518d fix unit tests 2020-08-12 14:07:20 -06:00
Jacob Beck
c5a19ca42e bump requirements, enable the token cache 2020-08-12 14:00:37 -06:00
Jacob Beck
153eb7e9d3 flake8 update found more things to complain about 2020-08-12 07:44:52 -06:00
Jacob Beck
7ba52d4931 mypy is rightfully mad about our funky inheritance, just copy+paste things
We can fix this when we drop python 3.6 and unions stop collapsing types
2020-08-12 07:44:52 -06:00
Jacob Beck
ebe5b46653 Add state:modified and state:new selectors 2020-08-12 07:44:51 -06:00
Jacob Beck
1bd82d4914 Merge pull request #2694 from kconvey/kconvey-retry-upstream
Add retry of additional errors
2020-08-12 07:43:50 -06:00
Jeremy Cohen
89775fa94f Merge pull request #2594 from brunomurino/issue-2533
added option --adapter to dbt init, to create sample profiles.yml bas…
2020-08-11 18:27:02 -04:00
Kurt Convey
4456872635 Didn't forget to add myself to the contributors 2020-08-11 15:23:02 -06:00
Kurt Convey
ee9ae22651 Fix variable name 2020-08-11 15:16:15 -06:00
Kurt Convey
afe0f46768 use isinstance with tuple 2020-08-11 15:15:04 -06:00
Kurt Convey
203d8c3481 Update CHANGELOG 2020-08-11 12:42:31 -06:00
Kurt Convey
c9ae49255d Add test from dev 2020-08-11 12:29:52 -06:00
Bruno Murino
51f17d3358 Update plugins/redshift/dbt/include/redshift/sample_profiles.yml
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-08-10 13:38:08 +01:00
Bruno Murino
cacdd58b41 Update plugins/redshift/dbt/include/redshift/sample_profiles.yml
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-08-10 13:38:01 +01:00
Bruno Murino
8b7bcbbc47 Update plugins/postgres/dbt/include/postgres/sample_profiles.yml
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-08-10 13:37:49 +01:00
Bruno Murino
bdf9482e75 Update plugins/postgres/dbt/include/postgres/sample_profiles.yml
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-08-10 13:37:42 +01:00
Bruno Murino
b94d0b66e6 Apply suggestions from code review
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-08-06 19:11:12 +01:00
Bruno Murino
1dff122a94 Update plugins/postgres/dbt/include/postgres/sample_profiles.yml
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-08-06 19:10:34 +01:00
Jacob Beck
fb8065df27 Merge pull request #2686 from fishtown-analytics/feature/override-core-macros
include project macros in the manifest the adapter stores locally
2020-08-06 11:35:25 -06:00
Jacob Beck
4274139210 Fix the test
Make sure "dbt deps" reloads the full manifest
Make sure commands that reload the dbt_project.yml properly reset the config (including adapters)
2020-08-05 14:50:59 -06:00
brunomurino
b422a44c03 update redshift and snowflake sample profiles 2020-08-05 19:56:29 +01:00
Jacob Beck
285479c0bc include project macros in the manifest the adapter stores locally 2020-08-05 09:35:00 -06:00
Jacob Beck
e641ec12fa Merge pull request #2684 from fishtown-analytics/feature/dispatch-schema-tests
convert tests
2020-08-05 09:27:55 -06:00
Jeremy Cohen
36fda28a92 Merge pull request #2677 from bbhoss/bq_impersonate
Add support for impersonating a service account with BigQuery
2020-08-04 14:37:53 -04:00
Jacob Beck
1ece515074 Merge pull request #2679 from fishtown-analytics/feature/adapter-dispatch
Create adapter.dispatch
2020-08-04 12:01:00 -06:00
Jacob Beck
04f840d907 convert tests 2020-08-04 09:47:48 -06:00
Preston Marshall
df8ccc04eb add contributors section 2020-08-04 10:49:35 -04:00
Jacob Beck
dd764b93e0 check if packages is a string and error out 2020-08-04 07:46:50 -06:00
Jacob Beck
335497f688 fixed error message, fixed the changelog 2020-08-03 15:48:04 -06:00
Jacob Beck
41a9251982 pr feedback: better errors for dotted macro names
add more tests
2020-08-03 12:31:29 -06:00
Preston Marshall
84bf03580d Merge remote-tracking branch 'upstream/dev/marian-anderson' into bq_impersonate 2020-08-03 13:00:13 -04:00
Preston Marshall
43d5dfcb71 move changelog additions to next 2020-08-03 12:57:30 -04:00
Preston Marshall
7a9fc7ef12 update changelog 2020-08-03 12:55:42 -04:00
Jacob Beck
ecf24cd4d9 adapter_macro -> adapter.dispatch
Added tests
Added deprecation warning + tests
2020-08-03 10:49:28 -06:00
Jacob Beck
ba828e59de Merge pull request #2675 from fishtown-analytics/feature/python-adapter-macro
Port the adapter macro to python
2020-08-03 09:50:13 -06:00
Jacob Beck
34719f94b4 PR feedback 2020-08-03 09:15:28 -06:00
Preston Marshall
c95a6792e5 add a test 2020-08-01 15:40:48 -04:00
Preston Marshall
88529d5c25 first try 2020-08-01 15:12:00 -04:00
Jacob Beck
3489878395 changelog 2020-07-31 12:19:51 -06:00
Jacob Beck
d938013733 remove unused to_dict method 2020-07-31 12:19:51 -06:00
Jacob Beck
dfe2ea4d0a adapter macro -> python
Updated tests
2020-07-31 12:19:51 -06:00
Jacob Beck
7a3e345cf7 upgrade mypy, now it might not crash on failures 2020-07-31 12:19:51 -06:00
Jacob Beck
1fb8c9c896 Merge pull request #2656 from fishtown-analytics/feature/defer-to-prod
Feature/defer to prod
2020-07-31 12:09:25 -06:00
Jacob Beck
2ed97640d5 changelog updates 2020-07-31 09:24:51 -06:00
Jacob Beck
6d31aa6f67 PR feedback
- Actually set deferred=True
- add a test for that
- combine/unify environment vars vs cli vars behavior between defer/state better
  - make the state path start with DBT_
2020-07-31 09:24:31 -06:00
Jacob Beck
a76ffbd194 add logging, add tests, update changelog 2020-07-31 09:24:31 -06:00
Jacob Beck
0d7e0ba087 read json and merge things 2020-07-31 09:24:31 -06:00
Github Build Bot
618d491f92 Release dbt v0.18.0b2 2020-07-30 17:11:32 +00:00
Jacob Beck
21a3462798 Merge pull request #2589 from azhard/dbt-2586-bigquery-policy-tags
Added support for setting policy tags for columns in BigQuery
2020-07-30 11:07:16 -06:00
Azhar Dewji
d2c5783e5a Addressed PR commments 2020-07-30 11:02:23 -04:00
Azhar Dewji
007cb4516a Addressed PR comments 2020-07-30 10:55:52 -04:00
Azhar Dewji
8ec93d9973 bigquery integration test fix 2020-07-30 10:55:52 -04:00
Azhar Dewji
0a64708302 Linting fix 2020-07-30 10:55:52 -04:00
Azhar Dewji
b964f6dd22 Rebased 2020-07-30 10:55:52 -04:00
Azhar Dewji
14233188be Changed update_column to update_columns 2020-07-30 10:55:51 -04:00
Azhar Dewji
e3193c6243 Updated pull request # 2020-07-30 10:55:51 -04:00
Azhar Dewji
8782714ec3 Added support for setting policy tags for columns in BigQuery 2020-07-30 10:55:51 -04:00
Jacob Beck
03010ecde7 Merge branch 'dev/0.17.2' into dev/marian-anderson 2020-07-29 13:54:10 -06:00
Github Build Bot
97d63d3da4 Release dbt v0.17.2 2020-07-29 19:09:25 +00:00
Jacob Beck
a212f4eb5e Merge pull request #2663 from fishtown-analytics/fix/changelog
Fix the changelog
2020-07-29 12:47:26 -06:00
Jacob Beck
5c993fd568 fix the changelog 2020-07-29 12:10:35 -06:00
Jeremy Cohen
df60189266 Merge pull request #2658 from fishtown-analytics/docs/0.17.2-code-block-background
Docs site updates for 0.17.2: fix code block background
2020-07-29 10:50:36 -04:00
Jacob Beck
97f61a9ecc Merge pull request #2654 from fishtown-analytics/fix/redshift-schema-override-catalog
fix redshift catalog with custom schemas
2020-07-29 08:39:16 -06:00
Jeremy Cohen
8740e6ca66 Docs site updates for 0.17.2: fix code block background 2020-07-28 18:43:09 -04:00
Claire Carroll
c9eb3c2b54 Merge pull request #2623 from fishtown-analytics/asset-paths
Compile assets as part of docs generate
2020-07-28 17:15:21 -04:00
Github Build Bot
d9411bc9bb Release dbt v0.17.2rc1 2020-07-28 20:20:16 +00:00
Claire Carroll
a3a1b143cf Compile assets as part of docs generate 2020-07-28 15:49:15 -04:00
Jacob Beck
5c18c78b50 Merge pull request #2650 from fishtown-analytics/fix/close-connections
Fix: close connections after use
2020-07-28 13:25:24 -06:00
Jacob Beck
3bcb5f147d fix a test with too-strict bounds, any <=2 value is ok here 2020-07-28 13:02:34 -06:00
Jacob Beck
1bfe43ff76 PR feedback:
- fix conftest adainivalue_line calls
 - deprecate the release argument to execute_macro
2020-07-28 12:07:08 -06:00
Jacob Beck
2d2019c059 add a test for cross-schema catalog generation, fix redshift to work for schema overrides 2020-07-28 11:08:24 -06:00
Jacob Beck
4613619841 missed the snowflake rpc tests 2020-07-28 07:55:24 -06:00
Jacob Beck
44e3c7eb6d only try to cancel open connections 2020-07-27 16:08:43 -06:00
Jacob Beck
fd0b460391 alter the tests so we can run rpc tests on snowflake 2020-07-27 15:16:14 -06:00
Jacob Beck
3b917b9d79 Handle the fallout of closing connections in release
- close() implies rollback, so do not call it
- make sure to not open new connections for executors in single-threaded mode
- logging cleanups
- fix a test case that never acquired connections
- to cancel other connections, one must first acquire a connection for the master thread
- change a number of release() calls to rollback

release vs rollback
2020-07-27 15:16:14 -06:00
Jacob Beck
970049fde8 Always close connections in release() 2020-07-27 15:16:13 -06:00
Jacob Beck
5b40cc4a01 Merge pull request #2649 from fishtown-analytics/fix/dbt-clean-noprofile
Fix: dbt clean without profile
2020-07-27 15:15:13 -06:00
Jacob Beck
0919c5642d Update CHANGELOG.md
Co-authored-by: Jeremy Cohen <jeremy@fishtownanalytics.com>
2020-07-27 13:42:21 -06:00
Jacob Beck
de4f90b3f4 clean without a profile 2020-07-27 11:11:38 -06:00
Jacob Beck
a1c2270956 azure pipelines silently messing with $PATH again 2020-07-27 11:04:50 -06:00
Jacob Beck
b9cf0cfc44 Merge pull request #2646 from joshpeng-quibi/fix/fail-fast-output
Fix fast-fail not logging original error message
2020-07-27 11:02:26 -06:00
joshpeng-quibi
d2869e4749 Apply suggestions from code review
Co-authored-by: Jacob Beck <beckjake@users.noreply.github.com>
2020-07-24 13:49:18 -07:00
Jacob Beck
3ec911b62c Merge pull request #2640 from fishtown-analytics/feature/yaml-selections
Feature/yaml selections
2020-07-24 14:30:32 -06:00
Josh Peng
4ff66c1cb6 Fix fast-fail not logging original error message 2020-07-24 12:06:57 -07:00
Jacob Beck
184146b825 add changelog 2020-07-22 09:57:27 -06:00
Jacob Beck
1e8543efb1 Create selector config file, handle selector argument
Added tests
Added RPC and CLI support
2020-07-22 09:31:55 -06:00
Jacob Beck
a976e54c19 Parse selectors
This doesn't use hologram, as it needs more permissive schemas than what hologram provides.
Added some unit tests
2020-07-22 07:58:10 -06:00
Jacob Beck
88e26da026 Merge pull request #2629 from fishtown-analytics/feature/new-node-selectors
new node selectors (#2425)
2020-07-21 20:33:48 -06:00
Jacob Beck
58522b8f67 PR feedback: catch non-declared config items 2020-07-21 15:43:52 -06:00
Github Build Bot
f09bb17da4 Release dbt v0.17.2b1 2020-07-21 13:28:43 +00:00
Jacob Beck
353ae0f090 changelog fix 2020-07-21 07:25:42 -06:00
Jacob Beck
0404881d85 Merge pull request #2635 from fishtown-analytics/feature/debug-logging-env-vars
add environment variables for logging
2020-07-20 13:48:18 -06:00
Jacob Beck
fb88d54c31 Merge pull request #2628 from fishtown-analytics/feature/advanced-node-selection
split up node selection
2020-07-20 13:48:04 -06:00
Jacob Beck
a523356504 new node selectors
- added a bunch of unit tests
- added some new integration tests
- cleaned up some selection logic
2020-07-20 11:16:40 -06:00
Jacob Beck
91ff7f1c80 PR feedback: only log about the innermost failed expectation 2020-07-20 11:16:17 -06:00
Jacob Beck
51f6c268d3 add environment variables for logging 2020-07-20 09:55:19 -06:00
Jacob Beck
5e1b284ad0 Bump version: 0.17.1 → 0.17.2a1 2020-07-20 09:39:04 -06:00
Github Build Bot
a962d28674 Release dbt v0.17.1 2020-07-20 12:48:01 +00:00
Jacob Beck
7f0c1f823a Merge pull request #2627 from kning/ISSUE-2569_bad_target
Add more helpful error message for misconfiguration in profiles.yml
2020-07-17 11:48:08 -06:00
Kenny Ning
d08a2b8c02 Add PR link to changelog 2020-07-17 11:49:38 -04:00
Kenny Ning
d7f3518a9a Add test 2020-07-17 11:37:02 -04:00
Jacob Beck
d34d2a023c pr feedback: Make UniqueId a NewType 2020-07-17 09:03:39 -06:00
Kenny Ning
1ecd954199 Address review 2020-07-16 16:08:02 -04:00
Jacob Beck
6e161ab84e azure pipelines silently messing with $PATH again 2020-07-15 17:25:14 -06:00
Jacob Beck
97c59f36a2 fix some bugs
rpc, ls, some import tweaks
2020-07-15 16:30:38 -06:00
Jacob Beck
89443eac0e Refactor for node selection
- moved what remains of the linker into compilation, migrate things to the graph
 - compile uncompiled ephemeral models when prepending ctes
  - no more including them all in the selection
 - per-node task selectors
 - added the idea of a selection spec, graph selection uses that
   - right now we parse include + exclude strings
2020-07-15 16:30:38 -06:00
Jacob Beck
3e8414e259 merge dev/0.17.1 2020-07-15 15:49:40 -06:00
Kenny Ning
06a30f0447 Add more helpful error message for misconfiguration in profiles.yml 2020-07-15 17:26:12 -04:00
Jacob Beck
4e636aa53a Merge pull request #2610 from fishtown-analytics/feature/adapter-test-suite-changes
move the sql for getting different rows into dbt proper
2020-07-15 08:43:59 -06:00
Claire Carroll
7bd18ef331 Merge pull request #2576 from fishtown-analytics/update-logo
Update logo
2020-07-10 10:24:52 -04:00
Github Build Bot
181fcc3965 Release dbt v0.17.1rc4 2020-07-08 16:12:55 +00:00
Jacob Beck
3a1f745e65 Merge pull request #2618 from fishtown-analytics/fix/native-env-opt-in
make native env rendering opt-in
2020-07-08 09:44:57 -06:00
Jacob Beck
2dc3ea3f12 fix tests 2020-07-08 08:41:58 -06:00
Jacob Beck
1e8ed49661 Apply suggestions from code review
Co-authored-by: Drew Banin <drew@fishtownanalytics.com>
2020-07-08 08:34:39 -06:00
Jacob Beck
84bf169458 make native env rendering opt-in 2020-07-07 14:50:40 -06:00
brunomurino
3dabe62254 updated redshift and snowflake sample_profiles.yml 2020-07-04 14:13:51 +01:00
brunomurino
0d246ac95b updated postgres sample_profiles.yml 2020-07-02 21:16:45 +01:00
brunomurino
2d0612c972 updated bigquery sample_profiles.yml 2020-07-02 21:14:52 +01:00
Jacob Beck
4cf2b78fca move the sql for getting different rows into dbt proper, from the test suite. Bump pytest dependency. 2020-07-02 08:39:23 -06:00
Jacob Beck
3af8a22d13 Merge pull request #2590 from fishtown-analytics/fix/adapter-macro-namespacing
Fix adapter macro namespacing (#2548)
2020-07-01 10:51:29 -06:00
Jacob Beck
b0c7b3a38b remove commented out code 2020-07-01 09:21:01 -06:00
Github Build Bot
3be03b681b Release dbt v0.17.1rc3 2020-07-01 13:19:07 +00:00
Jacob Beck
dd5b7240b6 Merge pull request #2601 from fishtown-analytics/fix/bigquery-seed-descriptions
On bigquery, persist docs for seeds as well as tables/views
2020-06-30 12:26:52 -06:00
Jacob Beck
2b9b82e5c4 Merge branch 'dev/0.17.1' into fix/bigquery-seed-descriptions 2020-06-30 09:38:13 -06:00
Jacob Beck
c5c14bb798 Merge pull request #2603 from fishtown-analytics/fix/windows-is-the-worst-os-ever
remove length check + catch any exceptions on windows
2020-06-30 09:04:26 -06:00
Jacob Beck
a201fc773a Merge pull request #2599 from fishtown-analytics/fix/more-jinja-quoting-sadness
Fix more jinja quoting sadness
2020-06-30 07:53:42 -06:00
Jacob Beck
d897164d5c PR feedback: add exception to the log message 2020-06-30 07:53:23 -06:00
Jacob Beck
e558d3f9ad remove length check + catch any exceptions on windows 2020-06-30 07:42:41 -06:00
Jacob Beck
6a7078f634 flake8 2020-06-29 16:02:49 -06:00
Jacob Beck
b14676f0c5 Merge branch 'dev/0.17.1' into fix/more-jinja-quoting-sadness 2020-06-29 15:57:07 -06:00
Jacob Beck
4cef1af8cb On bigquery, persist docs for seeds as well as tables/views 2020-06-29 15:50:48 -06:00
Jacob Beck
42aa407adc PR feedback: extra test 2020-06-29 15:00:00 -06:00
Claire Carroll
e7298d60d3 Merge pull request #2600 from fishtown-analytics/fix/package-tracking
Hash local package name
2020-06-29 16:38:32 -04:00
Claire Carroll
9c0ff932c8 Update changelog 2020-06-29 15:56:03 -04:00
Claire Carroll
288dd221f8 Hash local package name 2020-06-29 15:39:17 -04:00
Jacob Beck
d374ef9ffe If it comes into literal_eval as a string and it comes out as a string, return the original 2020-06-29 13:18:52 -06:00
Jeremy Cohen
7fa6a363b2 Merge pull request #2577 from fishtown-analytics/refactor/contributing
Revise CONTRIBUTING doc
2020-06-26 14:42:47 -04:00
brunomurino
13da3390e5 updated changelog 2020-06-26 18:58:45 +01:00
brunomurino
4164e6ee8e updated setup.py of adapters to include sample_profiles.yml 2020-06-26 18:12:18 +01:00
brunomurino
2b454f99dd updated 040_init_test for profile postgres to work with new --adapter option of dbt init 2020-06-25 23:54:33 +01:00
brunomurino
2025634417 added option --adapter to dbt init, to create sample profiles.yml based on chosen adapter 2020-06-25 23:27:08 +01:00
Github Build Bot
42e582a6ad Release dbt v0.17.1rc2 2020-06-25 21:05:41 +00:00
Drew Banin
339a7e9409 Merge pull request #2588 from fishtown-analytics/docs/new-logo-jun-2020
Docs/new logo jun 2020
2020-06-25 17:02:52 -04:00
Jacob Beck
f420911e19 Merge pull request #2591 from fishtown-analytics/fix/windows-path-length-nonsense
More windows path lengths
2020-06-25 14:57:09 -06:00
Jacob Beck
2e39a5fff0 handle unmarked path too long errors 2020-06-25 10:32:42 -06:00
Jacob Beck
e0f7deabfd spelling 2020-06-25 10:26:09 -06:00
Jacob Beck
abe345e925 Allow plugin macros to override core
Moved some relation logic out of adapters and into contracts
Fixed a typo in a type name
Fixed an issue where tests could fail based on color settings
Fixed the type analysis for adapter plugins/factory
Swapped more concrete tyeps out for protocols
Finally removed PACKAGES global
Added unit tests
2020-06-25 10:05:41 -06:00
Jacob Beck
4d33554465 protocols 2020-06-25 10:02:03 -06:00
Jacob Beck
32c559838d Try to make imports a little more sane, ordering-wise
consolidate dbt.ui, move non-rpc node_runners into their tasks
move parse_cli_vars into config.utils
get rid of logger/exceptions requirements in dbt.utils
2020-06-25 10:02:02 -06:00
Jacob Beck
62a0bf8732 store package info on the factory instead of globally
adapter_types -> plugins
Use factory.packages instead of global PACKAGES
2020-06-25 09:59:47 -06:00
Jacob Beck
24ae8b4498 Merge pull request #2555 from DrMcTaco/snowflake-query-tag
Snowflake query tag
2020-06-25 08:46:27 -06:00
Jacob Beck
2945a91189 Merge branch 'dev/marian-anderson' into snowflake-query-tag 2020-06-25 07:36:45 -06:00
Drew Banin
26735dac8d update docs logo 2020-06-25 09:35:13 -04:00
Jeremy Cohen
7c0a41c998 Fixup anchor 2020-06-25 00:35:39 -04:00
Jeremy Cohen
778a89ef8a Feedback from Andrew 2020-06-25 00:34:31 -04:00
Jeremy Cohen
83d85dc928 Merge pull request #2581 from brunomurino/issue-2437
added option to specify profile when connecting to redshift via IAM
2020-06-24 10:13:34 -04:00
Dan Rubin
25a06aee01 Merge branch 'dev/marian-anderson' into snowflake-query-tag 2020-06-24 09:16:07 -04:00
Dan Rubin
c89ea1ad56 fix CHANGELOG formatting 2020-06-24 09:15:17 -04:00
Jeremy Cohen
76f9f23b16 Merge pull request #2530 from alepuccetti/bigquery-processed-bytes-log
Add data processed info into dbt run logs for all statement types
2020-06-23 16:39:33 -04:00
Jacob Beck
a45f6bea5b Merge pull request #2584 from fishtown-analytics/fix/plus-prefixed-hooks
Fix prefixing support to not render "+pre-hook"
2020-06-23 14:37:08 -06:00
Dan Rubin
49fec95b33 Merge branch 'dev/marian-anderson' into snowflake-query-tag 2020-06-23 16:20:44 -04:00
Jacob Beck
d38fd80dff PR feedback: double seeds 2020-06-23 13:25:01 -06:00
Jacob Beck
37b03ef5d2 Fix prefixing support to not render "+pre-hook"
Increased the startup timeout in the RPC tests to try to fix some local timeouts
2020-06-23 10:04:23 -06:00
Bruno Murino
0c878a056e Update CHANGELOG.md
Co-authored-by: Jacob Beck <beckjake@users.noreply.github.com>
2020-06-23 16:24:25 +01:00
Alessandro Puccetti
1da4bef068 Update CHANGELOG.md
Co-authored-by: Jeremy Cohen <jtcohen6@gmail.com>
2020-06-23 16:47:37 +02:00
brunomurino
65c156750d updated changelog 2020-06-23 15:09:10 +01:00
brunomurino
bd34dfe1f7 removed hardcoded option in fetch_cluster_credentials 2020-06-23 14:53:53 +01:00
brunomurino
78445f9879 updates to make pep8 compliant 2020-06-22 23:15:40 +01:00
brunomurino
053210330e updated to comply with pep8 guidelines 2020-06-22 23:03:14 +01:00
brunomurino
df894f91f2 updated changelog 2020-06-22 22:27:57 +01:00
Jacob Beck
f758614b0a Merge pull request #2579 from christineberger/feature/change_yml_warning_color
Feature: Change project-level warning color
2020-06-22 14:41:39 -06:00
Bruno Murino
71846c98b3 added option to specify profile when connecting to redshift via IAM 2020-06-22 21:39:03 +01:00
Alessandro Puccetti
272cad7b97 core/dbt/utils: remove extra empty line 2020-06-22 22:21:02 +02:00
Alessandro Puccetti
5eb9523325 plugins/bigquery: log processed bytes value for all statement types
Fixes https://github.com/fishtown-analytics/dbt/issues/2526
2020-06-22 22:21:02 +02:00
Alessandro Puccetti
d2b2f2f6b6 core/dbt/utils: add format_rows_number 2020-06-22 22:21:01 +02:00
Alessandro Puccetti
55f9720a44 core/dbt/utils: add PB to format_bytes and always return the accurate value 2020-06-22 22:17:06 +02:00
Jeremy Cohen
9d52e6acbf Feedback from Sanjana 2020-06-22 14:57:05 -04:00
Jacob Beck
8f649f5ab4 Merge pull request #2547 from azhard/bigquery-alter-column-type
Add support for altering BigQuery column types
2020-06-22 12:50:18 -06:00
Christine
fbfb54e3e2 Fixed flake8 suggestions on inline comment 2020-06-22 12:51:39 -05:00
Christine
e523d908bf Moved dbt.tracking InvocationProcesser in to print_run_end_messages() to avoid import loop 2020-06-22 12:45:24 -05:00
Christine
8b2d416035 Simplified warning_tag() logic 2020-06-22 11:56:51 -05:00
Jeremy Cohen
078356458b Feedback from Drew 2020-06-22 12:38:01 -04:00
Christine
4846246f12 Fixed indenting on end cap of function 2020-06-22 11:30:33 -05:00
Christine
dc10b3d904 Fixed more flake8 whitespace suggestions 2020-06-22 11:27:59 -05:00
Christine
05d4a5e4a3 Updated old warning_tag concatenation to use new warning_tag function 2020-06-22 11:17:46 -05:00
Christine
2883b6dc17 Removed some formatting and whitespace 2020-06-22 11:16:31 -05:00
Christine
5f9e153db8 Fixed whitespace suggestions from flake8 2020-06-22 11:04:09 -05:00
Christine
c05153f00a Added changes to changelog.md 2020-06-22 09:38:05 -05:00
Claire Carroll
3616641e93 Update other logos 2020-06-22 08:48:59 -04:00
Christine
78cbeca606 Added some whitespace under the deprecation warning 2020-06-20 15:09:06 -05:00
Christine
354c7ecddf Changed configuration paths warning to be wrapped with new warning_tag 2020-06-20 15:08:44 -05:00
Christine
4d659318c4 Changed format of project-level warning messages to be wrapped with new warning_tag 2020-06-20 15:07:58 -05:00
Christine
9d4399eb28 Added a warning_tag wrapper fuction to printer class 2020-06-20 15:06:48 -05:00
Github Build Bot
32702559f6 Release dbt v0.17.1rc1 2020-06-19 21:13:16 +00:00
Drew Banin
310a1dfaec Merge pull request #2565 from fishtown-analytics/docs/0.17.1-docs-updates
bump docs site
2020-06-19 17:08:55 -04:00
Drew Banin
6528b5f25b Merge branch 'dev/0.17.1' into docs/0.17.1-docs-updates 2020-06-19 17:08:44 -04:00
Jacob Beck
19378abfae Merge pull request #2575 from fishtown-analytics/fix/backport-aliasing
backport aliases in config fix
2020-06-19 15:06:57 -06:00
Jacob Beck
593e86c15d Merge pull request #2550 from bodschut/bodschut/bq_nested_column_descriptions
Add functionality to persist descriptions to nested columns in bigquery
2020-06-19 13:58:21 -06:00
Jeremy Cohen
6226d60c9a Give CONTRIBUTING.md some love 2020-06-19 15:36:38 -04:00
Claire Carroll
aa35dc9437 Update logo 2020-06-19 15:25:28 -04:00
Jacob Beck
098d05c27f Fix tests 2020-06-19 12:54:10 -06:00
Jacob Beck
05917c6d2f update changelog 2020-06-19 11:29:35 -06:00
Jacob Beck
e300c62d76 add a test 2020-06-19 11:24:47 -06:00
Jacob Beck
3829d1f964 Fix a regression where dbt ignored aliases in config() calls 2020-06-19 11:16:07 -06:00
Bob De Schutter
0d2593e0e3 Fix docs_generate test 2020-06-19 18:19:50 +02:00
Bob De Schutter
e99f0d12b8 Remove commented out code
Co-authored-by: Jacob Beck <beckjake@users.noreply.github.com>
2020-06-19 16:56:17 +02:00
Jacob Beck
d0e736f31b Merge branch 'dev/0.17.1' into bodschut/bq_nested_column_descriptions 2020-06-19 08:41:19 -06:00
Bob De Schutter
3a5db3c16d Updated changelog 2020-06-19 16:37:53 +02:00
Jacob Beck
0cddd9c417 Merge pull request #2566 from fishtown-analytics/fix/windows-long-paths
Fix windows long paths
2020-06-19 08:31:18 -06:00
Bob De Schutter
51ce3e1093 Fix persist docs functionality for bigquery to include descriptions for nested columns 2020-06-19 16:31:14 +02:00
Jacob Beck
db50880399 fix this test for spawn() vs fork() 2020-06-19 07:26:40 -06:00
Jacob Beck
8e63da2f04 Be pickier about when to mangle paths, handle the case where we still hit path length limits 2020-06-18 17:11:15 -06:00
Drew Banin
a0b502dd44 Merge branch 'dev/0.17.1' into docs/0.17.1-docs-updates 2020-06-18 17:34:42 -04:00
Jacob Beck
ff272c797a make sure dbt clean still works... 2020-06-18 15:17:27 -06:00
Jacob Beck
f4b93c8a5a handle long paths in windows, add a test 2020-06-18 15:08:43 -06:00
Jacob Beck
c01056b99a Merge pull request #2559 from fishtown-analytics/fix/alias-bug
Fix a regression where dbt ignored aliases in config() calls
2020-06-18 07:13:36 -06:00
Jeremy Cohen
12ca721e0b Merge pull request #2567 from fishtown-analytics/feature/ci-on-forks
Run all integration tests on PRs from forks
2020-06-17 18:57:07 -04:00
Drew Banin
9c7d519ab2 changelog 2020-06-17 16:32:57 -04:00
Drew Banin
5299079f01 bump docs site 2020-06-17 16:30:30 -04:00
Jeremy Cohen
4c8575dea3 Build PRs on forks 2020-06-17 15:01:50 -04:00
Dan Rubin
f3aaa8bcee Merge branch 'snowflake-query-tag' of https://github.com/DrMcTaco/dbt into snowflake-query-tag 2020-06-17 13:02:18 -04:00
Dan Rubin
2cc53321ae remove incorrect 2nd arg to config.get 2020-06-17 13:01:16 -04:00
Jacob Beck
64e9d704e2 Merge pull request #2554 from fishtown-analytics/fix/parallel-rpc-method-stomping
Fix parallel rpc methods (2484)
2020-06-17 10:54:09 -06:00
Jacob Beck
1c1f10056d Fix a regression where dbt ignored aliases in config() calls 2020-06-17 07:41:19 -06:00
Dan Rubin
ccae4a8888 Merge branch 'dev/marian-anderson' into snowflake-query-tag 2020-06-16 18:49:08 -04:00
Dan Rubin
5c1840914b update CHANGELOG.md 2020-06-16 18:43:58 -04:00
Dan Rubin
be61e6724a add query tag update macros to each snowflake materialization 2020-06-16 18:43:44 -04:00
Dan Rubin
38975ecee0 add snowflake macros to support fetching current session query tag, updating the session query tag, and reverting to the old query tag 2020-06-16 18:43:26 -04:00
Dan Rubin
d8b31c02e2 add query tag to the snowflake adapter config 2020-06-16 18:43:13 -04:00
Dan Rubin
331b961819 add query tag to snoflake connection dataclass and set tag on new connection if query_tag value is present 2020-06-16 18:42:56 -04:00
Azhar Dewji
769e4d5193 Added warning comment to alter macro 2020-06-16 18:11:08 -04:00
Jacob Beck
dbf7e690b8 this test difference appears to be spawn vs fork 2020-06-16 16:00:07 -06:00
Jacob Beck
12a9c2b441 changelog 2020-06-16 14:46:07 -06:00
Jacob Beck
12d3c52de2 add a test
For the test to work, we have to switch to spawn so that we don't deadlock
2020-06-16 14:43:39 -06:00
Jacob Beck
614f0b4350 deepcopy args in method calls 2020-06-16 13:08:37 -06:00
Jacob Beck
74c78ef927 Merge branch 'dev/0.17.1' into dev/marian-anderson 2020-06-15 13:17:43 -06:00
Jacob Beck
765c2de148 Merge pull request #2537 from fishtown-analytics/fix/bump-minimum-python
Update minimum python version
2020-06-15 12:18:33 -06:00
Jacob Beck
644fef1e84 Update minimum python version: 3.6.2 has a typing bug that dbt/hologram handle poorly 2020-06-15 11:30:29 -06:00
Jacob Beck
b9053a24dc Merge pull request #2535 from fishtown-analytics/feature/allow-package-version-floats
allow floats for package versions, add a test with a float-like branch (#2518)
2020-06-15 11:29:55 -06:00
Jacob Beck
0be8326351 Merge pull request #2534 from fishtown-analytics/fix/dbt-modules-in-project-dir
Make deps respect --project-dir
2020-06-15 11:29:21 -06:00
Azhar Dewji
a7a73df696 Add support for altering BigQuery column types 2020-06-15 12:54:27 -04:00
Jacob Beck
e9b4c47887 PR feedback: use the project root instead of cwd 2020-06-15 10:07:58 -06:00
Jacob Beck
7ee6f1de39 allow relative paths in project-dir 2020-06-12 13:00:00 -06:00
Jacob Beck
eea51bee30 Merge pull request #2517 from azhard/bigquery-auth-views
Added support for BigQuery authorized views
2020-06-11 14:17:25 -06:00
Jacob Beck
0beb03fe53 allow floats for package versions, add a test with a float-like branch 2020-06-11 11:43:47 -06:00
Azhar Dewji
4ccfe020a2 Moved bigquery imports into bigquery test 2020-06-11 13:30:36 -04:00
Azhar Dewji
19735d5416 Added noqa flag to GrantTarget import 2020-06-11 13:19:05 -04:00
Azhar Dewji
e53a45b3e0 Added integration test 2020-06-11 13:10:10 -04:00
Jacob Beck
e00d1ed78c make tests drop more things at cleanup
Make the unique ID a bit more collision-resistant to try to avoid collisions
2020-06-11 09:58:06 -06:00
Jacob Beck
83a4562b98 Make deps respect --project-dir 2020-06-11 07:39:12 -06:00
Jacob Beck
27fef50919 Merge pull request #2528 from fishtown-analytics/fix/do-not-create-schemas
Fix/do not create schemas
2020-06-11 07:36:15 -06:00
Jacob Beck
947a00f6c9 Bump version: 0.17.0 → 0.17.1a1 2020-06-10 15:11:31 -06:00
Jacob Beck
2f36d7d259 Update changelog 2020-06-10 15:08:46 -06:00
Jacob Beck
e2d7027074 add a test 2020-06-10 15:07:15 -06:00
Jacob Beck
a2de92d2a3 Fix azure pipelines builds by using the existing postgres setup 2020-06-10 15:07:15 -06:00
Jacob Beck
edef65f333 do not create schemas 2020-06-10 15:07:15 -06:00
Azhar Dewji
e7dfe97ecc Debug message change 2020-06-10 14:33:29 -04:00
Azhar Dewji
cd0fe51109 Re-add dropped changelog lines 2020-06-10 11:55:51 -04:00
Azhar Dewji
6c0a5021cd Merge branch 'dev/marian-anderson' into bigquery-auth-views 2020-06-10 11:54:38 -04:00
Jacob Beck
c9b34681b7 Merge pull request #2521 from azhard/bigquery-rename-relation
Added support for BigQuery relation renaming
2020-06-10 09:48:28 -06:00
Azhar Dewji
8422c3a9d3 grant_access no longer common and takes in grant_target_dict param 2020-06-10 11:35:18 -04:00
Azhar Dewji
daed251d0d Fix seed file name 2020-06-09 17:04:42 -04:00
Azhar Dewji
ddb6d791e8 Fixed CI errors 2020-06-09 16:28:00 -04:00
Azhar Dewji
107170163a Fixed relation creation 2020-06-09 15:00:29 -04:00
Azhar Dewji
7aa09e393c Undo accidental commented out test 2020-06-09 14:50:08 -04:00
Azhar Dewji
75cc3e40a5 Addressed PR comments 2020-06-09 14:43:59 -04:00
Azhar Dewji
78cf84310c Changed dataset_id to relation 2020-06-09 11:58:24 -04:00
Azhar Dewji
0f6622fab6 Added support for BigQuery relation renaming 2020-06-09 11:17:17 -04:00
Azhar Dewji
bb20df0db9 #1718 - Added support for BigQuery authorized views 2020-06-08 17:30:26 -04:00
Github Build Bot
8a2b5f53f6 Release dbt v0.18.0b1 2020-06-08 21:27:13 +00:00
Jacob Beck
e30db9816e Merge branch 'dev/octavius-catto' into dev/marian-anderson 2020-06-08 14:52:29 -06:00
Github Build Bot
5f8efc3aa8 Release dbt v0.17.0 2020-06-08 17:12:47 +00:00
Jacob Beck
f3565f3f70 Merge pull request #2513 from southpolemonkey/dev/marian-anderson
(#2511) Remove log messages about opening connections in set_connecti…
2020-06-08 10:06:34 -06:00
Chenxuan Rong
2be7ee6ee0 update CHANGELOG 2020-06-08 23:42:22 +10:00
Chenxuan Rong
f7a8cd1250 (#2511) Move opening connection message to LazyHandle module 2020-06-08 15:17:07 +10:00
Chenxuan Rong
3f5cc224a6 (#2511) Move opening connection message inside open method 2020-06-05 08:18:36 +10:00
Jacob Beck
26363638bc Merge pull request #2505 from aburgel/move-pytest-logbook
Move pytest-logbook dependency to dev_requirements.txt
2020-06-04 10:37:30 -06:00
Chenxuan Rong
1cd51181eb (#2511) Remove log messages about opening connections in set_connection_name. 2020-06-05 00:53:02 +10:00
Alex Burgel
a1d5c340e8 Update changelog 2020-06-04 10:33:11 -04:00
Alex Burgel
6f1763317a Update changelog 2020-06-04 10:31:24 -04:00
Alex Burgel
8973393333 Move pytest-logbook dependency to dev_requirements.txt 2020-06-04 10:31:24 -04:00
Jacob Beck
a5d53c4453 Add dbt:0.17.0rc4 dockerfiles and requirements 2020-06-02 08:19:47 -06:00
Jacob Beck
85fda89413 Bump version: 0.17.0rc3 → 0.17.0rc4 2020-06-02 07:48:09 -06:00
Jacob Beck
7b6c11adb7 Changelog update for 0.17.0rc4 2020-06-02 07:47:38 -06:00
Jacob Beck
7e14666802 Merge pull request #2502 from fishtown-analytics/fix/homebrew-packaging
Generate the homebrew formula pypi urls in the build script (#2500)
2020-06-02 07:11:07 -06:00
Jacob Beck
6fbf6721b9 Merge pull request #2509 from fishtown-analytics/fix/snowflake-get-columns-missing-relation
fix get_columns_in_relation on snowflake (#2504)
2020-06-02 06:34:52 -06:00
Jacob Beck
ec76498686 PR feedback: remove extra class def 2020-06-02 06:34:40 -06:00
Jacob Beck
05634e61a7 Merge pull request #2508 from scarrucciu/patch-1
Fix --dependency arg bug in create_adapter_plugin
2020-06-01 12:43:29 -06:00
Spencer Carrucciu
2dd7ef5302 Update CHANGELOG.md 2020-06-01 12:47:15 -04:00
Jacob Beck
49a5a556e9 fix get_columns_in_relation on snowflake
Also add a test for adapter methods to catch this if it regresses
2020-06-01 10:28:42 -06:00
Spencer Carrucciu
855dd98c62 Fix --dependency arg bug in create_adapter_plugin
Fix bug in create_adapter_plugin when a dependency is not passed in.
2020-06-01 07:37:33 -04:00
Jacob Beck
2759c9e3bd Generate the homebrew formula pypi urls in the build script, check versions 2020-05-29 14:50:09 -06:00
Jacob Beck
594b8b786a Merge pull request #2501 from fishtown-analytics/fix/better-filename-errors
add project names/filenames/values to rendering errors (#2499)
2020-05-29 11:30:28 -06:00
Jacob Beck
3387d085b4 add project names/filenames/values to rendering errors 2020-05-29 10:21:38 -06:00
Jacob Beck
ee6b842f9a Merge pull request #2491 from fishtown-analytics/fix/pin-protobufs
Put protobuf in version jail
2020-05-27 10:52:07 -06:00
Jacob Beck
fce11d7b46 Pin protobuf 2020-05-27 10:00:16 -06:00
Jacob Beck
48c9b8abdc Merge branch 'fix/catalog-allow-none' into dev/octavius-catto 2020-05-27 08:01:05 -06:00
Jacob Beck
c3c99f317e Merge pull request #2485 from Raalsky/direct-nodes-selector
Direct child model selector
2020-05-26 16:18:55 -06:00
Jacob Beck
9deb50f812 Add dbt:0.17.0rc3 dockerfiles and requirements 2020-05-26 14:55:16 -06:00
Jacob Beck
18c02ea991 Bump version: 0.17.0rc2 → 0.17.0rc3 2020-05-26 14:51:11 -06:00
Jacob Beck
3e23a5ff75 Merge pull request #2489 from fishtown-analytics/fix/catalog-allow-none
Make databases optional
2020-05-26 14:50:11 -06:00
Jacob Beck
076bb9c356 Allow column stats description fields to be empty 2020-05-26 14:06:31 -06:00
Jacob Beck
c21af17a30 Make databases optional 2020-05-26 11:59:20 -06:00
Raalsky
1d298ea5cf Added type annotations and errors types refactored 2020-05-26 19:03:29 +02:00
Raalsky
f214ebf4e9 Graph methods refactor 2020-05-26 17:55:45 +02:00
Raalsky
ce5ca6f845 Updated CHANGELOG 2020-05-24 17:05:06 +02:00
Raalsky
cc2be00abb Integration tests added for direct child model selection 2020-05-24 16:56:39 +02:00
Raalsky
bbfcce1cc6 Unit tests added for direct model selection 2020-05-24 16:42:53 +02:00
Raalsky
d81961ccee flake8 2020-05-24 16:07:49 +02:00
Raalsky
c1da5dd513 Added direct child syntax to node selector 2020-05-24 14:08:22 +02:00
Raalsky
7c201fed81 Added internal methods from networkx library 2020-05-24 12:10:15 +02:00
Raalsky
f3a97b6c08 Initial refactor of ancestors/descendants selector methods 2020-05-24 12:03:53 +02:00
Jacob Beck
6509067856 Add dbt:0.17.0rc2 dockerfiles and requirements 2020-05-22 14:22:13 -06:00
Jacob Beck
6482718dbf Bump version: 0.17.0rc1 → 0.17.0rc2 2020-05-22 14:18:45 -06:00
Jacob Beck
6dac4c76eb Merge pull request #2481 from fishtown-analytics/fix/docs-perf
Improve docs performance (#2480)
2020-05-22 14:16:43 -06:00
Jacob Beck
0d825eb590 Check for instances of "}}" and "%}" as well when deciding whether to render so users get compile errors instead of wrong data in that case 2020-05-22 13:34:32 -06:00
Jacob Beck
6cc0d9103d Add changelog entry, push another entry to its real release + add contributors entry 2020-05-21 16:55:06 -06:00
Jacob Beck
514a1ad7d0 On windows, docs now include their original carriage returns before newlines 2020-05-21 16:53:17 -06:00
Jacob Beck
7f99353ca0 add libyaml-dev so we get that libyaml goodness
And point tests to the new image
2020-05-21 16:53:17 -06:00
Jacob Beck
a58657403e avoid double-building the flat graph - we already built it at parse time 2020-05-21 16:53:17 -06:00
Jacob Beck
b9a86df795 if we have "cyaml" available, use it
This is a significant performance improvement with a lot of yaml files!
2020-05-21 16:53:17 -06:00
Jacob Beck
cf94ce6a54 add caching on the manifest for docs, ref, and source calls
removed the internal `find_*_by_name` calls for things that use the cache, just do them through `resolve_*`
fix unit tests to point at `resolve_*` now
2020-05-21 16:53:17 -06:00
Jacob Beck
e9d112ac39 Improve docs performance, especially on docs blocks that do not use jinja internally
Make docs parsing avoid a bunch of template nonsense and just use get_rendered
Add a check to get_rendered that skips the block if we can prove it should render to its input
  - but not in native mode
2020-05-21 16:53:17 -06:00
Jacob Beck
f4c6bf30b6 Merge pull request #2358 from ChristianKohlberg/feature/snowflake-expose-query-id
Expose Snowflake query id in case of an exception raised by connector
2020-05-21 11:35:42 -06:00
Jacob Beck
626522d1bb Merge pull request #2477 from fishtown-analytics/fix/global-vars
Fix some issues around global-scoped vars
2020-05-21 07:47:14 -06:00
Jacob Beck
58f3905af4 Merge pull request #2478 from fishtown-analytics/fix/source-patching-perf
fix source patching perf with no patches
2020-05-21 07:18:45 -06:00
ChristianKohlberg
89b4984009 Merge branch 'dev/octavius-catto' into feature/snowflake-expose-query-id 2020-05-20 23:15:38 +02:00
Christian Kohlberg
804c779359 expose snowflake query id if available 2020-05-20 23:12:05 +02:00
ChristianKohlberg
d699add216 Update CHANGELOG.md
Co-authored-by: Jacob Beck <beckjake@users.noreply.github.com>
2020-05-20 21:15:18 +02:00
Jacob Beck
e36fb3cc17 clean up replace() calls 2020-05-20 12:55:06 -06:00
Jacob Beck
fcf57a5066 if there is no patch, do not patch it 2020-05-20 12:48:38 -06:00
Jacob Beck
715c886dad appease flake8/mypy
Change variables named `l` to `log`
Make IsFQNResource runtime type-checkable and use that to handle the FQN check
Make the GenerateNameProvider its own top-level Provider type
2020-05-20 11:34:14 -06:00
Jacob Beck
e2cd45b18a Fix some issues around global-scoped vars
Fix global-level var rewriting from v2 -> v1
Add top-level vars to more lookup fields
When a user calls var() in a `generate_*_name` macro, on failure it now raises (like at runtime)
When a node has no FQN, pretend the FQN is just the package name when returning vars
Add tests
2020-05-20 10:59:15 -06:00
Jacob Beck
a8f8708418 Merge pull request #2475 from dmateusp/dmateusp/fix_docker_entrypoints
Fix ENTRYPOINT to use 'dbt'
2020-05-20 10:40:34 -06:00
Daniel Mateus Pires
ca5b02af9a Add Contributors 2020-05-20 16:32:06 +01:00
Daniel Mateus Pires
832d43efdc Add CHANGELOG note 2020-05-20 15:24:32 +01:00
Daniel Mateus Pires
e9daff7616 Fix ENTRYPOINT to use 'dbt' 2020-05-20 15:15:26 +01:00
Jacob Beck
22999a9861 Merge pull request #2467 from fishtown-analytics/fix/macos-tests
Fix tests to run on macos
2020-05-18 17:03:57 -06:00
Jacob Beck
64edf70b93 Fix tests to run on macos 2020-05-18 14:10:09 -06:00
Jacob Beck
75dbb0bc19 Merge pull request #2431 from alf-mindshift/dev/octavius-catto
Make it possible to hide macros in docs
2020-05-18 08:19:23 -06:00
Jacob Beck
e34439dcd7 Merge branch 'dev/marian-anderson' into dev/octavius-catto 2020-05-18 08:19:07 -06:00
Jacob Beck
9c84ce1e6f Merge pull request #2461 from fishtown-analytics/fix/typo-failfast-msg
Fix typo in failfast msg
2020-05-18 08:11:04 -06:00
Jacob Beck
907003881e Merge pull request #2464 from fishtown-analytics/fix/duplicate-sources-crash
Fix duplicate sources crash
2020-05-18 08:10:35 -06:00
Jacob Beck
4e2ec6b7f8 Merge pull request #2417 from Raalsky/feature/model-intersection-syntax
Model intersection syntax
2020-05-18 08:09:14 -06:00
Alf Lervag
4b90ff892f Updated tests and changelog 2020-05-18 15:25:52 +02:00
Alf Lervag
5591a82a15 Make it possible to hide macros in docs 2020-05-18 15:21:25 +02:00
Raalsky
598c06f5b0 CHANGELOG update 2020-05-16 00:12:38 +02:00
Raalsky
bf12001c04 Added concatenation and intersection unittests 2020-05-16 00:06:52 +02:00
Raalsky
e59df0fbb5 Added tests for model intersection syntax 2020-05-16 00:06:52 +02:00
Raalsky
47f5c51b2d Working prototype for model intersection syntax 2020-05-16 00:06:52 +02:00
Raalsky
e9b982b8ea Initial tests for model intersection syntax 2020-05-16 00:06:51 +02:00
Jacob Beck
770cf71c13 Merge branch 'dev/octavius-catto' into dev/marian-anderson 2020-05-15 15:00:55 -06:00
Jacob Beck
5734fb897c Merge pull request #2462 from fishtown-analytics/fix/version-2-test-intermittent-failures
Fix config-version 2 test intermittent failures
2020-05-15 15:00:33 -06:00
Jacob Beck
df8aa642c5 Fix crash on duplicate sources, add a test 2020-05-15 14:04:41 -06:00
Jacob Beck
5c76cfb071 remove the need for strict=False on most tests that have dependencies by upgrading the dependencies to config-version: 2 2020-05-15 11:58:26 -06:00
Jeremy Cohen
75f8bc2679 Fix typo in failfast msg 2020-05-15 13:36:51 -04:00
Jacob Beck
208c1cfb1a reset deprecation warnings after tests, clean up the fallout 2020-05-15 09:36:00 -06:00
Jacob Beck
1d543b3737 Merge pull request #2455 from fishtown-analytics/fix/adapter-plugin-config-version
set config-version: 2 in the generated plugin project
2020-05-14 15:05:39 -06:00
Jacob Beck
b41b72c316 set config-version: 2 in the generated plugin project 2020-05-14 14:07:42 -06:00
Jacob Beck
26703df4d7 Merge pull request #2450 from fishtown-analytics/fix/status-line-relations
In dbt run, log relations for the status line instead of explicit node info
2020-05-14 12:24:16 -06:00
Jacob Beck
f490f6af98 In dbt run, log relations for the status line instead of explicit node parts 2020-05-14 11:09:16 -06:00
Claire Carroll
70ad551cf4 Merge pull request #2446 from fishtown-analytics/slack-references
Replace slack references
2020-05-14 06:36:16 -04:00
Jacob Beck
48705b6d8c Merge pull request #2448 from fishtown-analytics/fix/bigquery-create-schema-macro
Fix create_schema macro on bigquery (#2445)
2020-05-13 14:40:49 -06:00
Jacob Beck
6e5c8750f6 Fix create_schema macro on bigquery 2020-05-13 13:35:11 -06:00
Claire Carroll
1f78f30756 Replace slack references 2020-05-13 13:56:27 -04:00
Drew Banin
df5b97c614 Merge pull request #2427 from RealAshampoo/feature/bigquery-cost-limit
Add BigQuery option to define a maximum query cost limit
2020-05-13 13:05:21 -04:00
Jacob Beck
1de892bf0e Merge pull request #2442 from fishtown-analytics/fix/empty-persist-docs
Backport #2440 to 0.17
2020-05-13 08:47:20 -06:00
Jacob Beck
457cbbd954 Merge pull request #2440 from fishtown-analytics/fix/empty-persist-docs
Fix empty SQL in persist docs (#2439)
2020-05-13 08:39:32 -06:00
haukeduden
842edd62a6 Merge branch 'dev/octavius-catto' into feature/bigquery-cost-limit 2020-05-13 09:55:11 +02:00
Jacob Beck
7b374a4b08 Merge pull request #2438 from fishtown-analytics/feature/full-refresh-config
make full refresh a config item
2020-05-12 15:28:19 -06:00
Jacob Beck
d0f5664358 PR feedback - config is still not a dict 2020-05-12 14:49:29 -06:00
Jacob Beck
053546402d Only run alter_column_comment if there are columns defined
Also remove unused (and confusing/misleading) test data
2020-05-12 14:42:37 -06:00
Jacob Beck
0fa9d0ab12 make full refresh a config item
Added integration tests, fixed existing tests
2020-05-12 12:10:03 -06:00
Jacob Beck
9d0eab6305 Bump version: 0.17.0rc1 → 0.18.0a1 2020-05-12 11:04:43 -06:00
Jacob Beck
79a90d0671 Add dbt:0.17.0rc1 dockerfiles and requirements 2020-05-12 10:51:35 -06:00
Jacob Beck
451e95e48d Bump version: 0.17.0b1 → 0.17.0rc1 2020-05-12 10:45:59 -06:00
Jacob Beck
d3f535a0a7 Changelog update 2020-05-12 10:45:17 -06:00
Jacob Beck
4fc54dae32 Merge pull request #2435 from fishtown-analytics/fix/no-testing-homebrew
Do not test homebrew packages
2020-05-12 10:44:46 -06:00
Jacob Beck
4e70d078ef Do not test homebrew packages 2020-05-12 09:50:37 -06:00
Drew Banin
31673d8acd Merge pull request #2433 from fishtown-analytics/docs/0.17.0-hide-from-search
Docs site updates for 0.17.0: Hide hidden models from search
2020-05-12 11:03:31 -04:00
Drew Banin
d5e127bfea Docs site updates for 0.17.0: Hide hidden models from search 2020-05-12 10:30:41 -04:00
Hauke Duden
f40374deb9 Merge branch 'dev/octavius-catto' into feature/bigquery-cost-limit 2020-05-12 16:15:22 +02:00
Hauke Duden
f8cbea4dbf added whitespace around operator to pass flake8 tests 2020-05-12 15:37:19 +02:00
Hauke Duden
e3bd6cd5ef - added pull request link to changelog
- added entry to contributors list
2020-05-12 15:35:57 +02:00
Drew Banin
71e7b0ab71 Merge pull request #2429 from fishtown-analytics/fix/track-package-deps-for-hub-site
Track project id for deps events
2020-05-12 09:32:51 -04:00
Drew Banin
fd662a1855 Merge pull request #2422 from azhard/fix/bigquery-view-spacing
Fix for extra spacing and parentheses for BigQuery views
2020-05-12 09:30:36 -04:00
Drew Banin
2a8940415d quiet new pep8 check 2020-05-11 20:57:00 -04:00
Drew Banin
b63a271d55 (#23551) track project id for deps events 2020-05-11 20:48:28 -04:00
Hauke Duden
47e70efca6 updated changelog 2020-05-11 20:16:58 +02:00
Hauke Duden
0f3dd88029 - added maximum_bytes_billed option to bigquery profiles
- added basic unit test for maximum_bytes_billed
2020-05-11 20:06:25 +02:00
Azhar Dewji
35a052f4ab Updated CHANGELOG links 2020-05-08 15:39:40 -04:00
Azhar Dewji
a31e78817e Re-add dropped semi-colon 2020-05-08 15:38:45 -04:00
Azhar Dewji
f250d7ab47 Fix for extra spacing and parentheses for BigQuery views 2020-05-08 15:36:29 -04:00
Jacob Beck
8686ab9a9d Merge pull request #2420 from fishtown-analytics/fix/dbt-init-v2-project
Fix dbt init to clone a v2 project (#2404)
2020-05-08 09:18:08 -06:00
Jacob Beck
2cd98c2c60 Fix dbt init to clone a v2 project 2020-05-08 08:41:28 -06:00
Jacob Beck
c9eec4fa06 Merge pull request #2411 from fishtown-analytics/fix/always-preserve-case
Preserve the case of schemas and databases when listing relations (#2403)
2020-05-08 08:15:07 -06:00
Drew Banin
6230c608d0 Merge pull request #2419 from fishtown-analytics/may-2020-update-readme
Update README.md
2020-05-07 23:11:12 -04:00
Jacob Beck
ab8392b856 Update core/dbt/task/runnable.py
Co-authored-by: Drew Banin <drew@fishtownanalytics.com>
2020-05-07 20:02:41 -06:00
Jacob Beck
067f02f9d8 Merge pull request #2418 from fishtown-analytics/fix/plugin-versions-search
fix version-loading logic to respect multiple plugins at the same path (#2410)
2020-05-07 19:10:04 -06:00
Drew Banin
0c1f8550b8 Update README.md 2020-05-07 20:07:02 -04:00
Jacob Beck
7287204279 fix up the unit tests 2020-05-07 14:32:52 -06:00
Jacob Beck
e392212c0e Change list_relations_without_caching macro to take a single argument
The argument is a Relation object with no identifier field, configured with the appropriate quoting information

Unique quoted/unquoted representations will be treated as distinct
The logic for generating what schemas to search for relations is now distinct from the catalog search logic.
Schema creation/dropping takes a similar relation argument
Add tests
2020-05-07 14:13:18 -06:00
Jacob Beck
dacfe38429 fix version-loading logic to respect multiple plugins at the same path 2020-05-07 14:08:43 -06:00
Jacob Beck
1eb75d9be7 Merge pull request #2416 from mikaelene/patch-1
Update CHANGELOG.md
2020-05-07 13:06:23 -06:00
Mikael Ene
a122d35e43 Update CHANGELOG.md
Added myself as contributor
2020-05-07 20:52:33 +02:00
Jacob Beck
445543e256 Merge pull request #2343 from Raalsky/warn-nothing-selected
Added warning to nodes selector if nothing was matched
2020-05-07 11:12:03 -06:00
Rafał Jankowski
cfeffe40f6 Typos corrected 2020-05-07 18:12:17 +02:00
Jacob Beck
dfb72ccb2e Merge pull request #2413 from azhard/fix/bigquery-list-schemas
Fix error when trying to call list_schemas macro for BigQuery adapter
2020-05-07 09:56:56 -06:00
Jacob Beck
0f4a369aeb Merge pull request #2414 from mikaelene/alias_returned_columns
alias for schema tests macros
2020-05-07 09:55:41 -06:00
Rafał Jankowski
385f7f2c24 Merge branch 'dev/octavius-catto' into warn-nothing-selected 2020-05-07 17:44:08 +02:00
Raalsky
aa36ed5aa4 CHANGELOG updated 2020-05-07 17:39:17 +02:00
Rafał Jankowski
c87096993e Nodes selector refactor and added warning for non-existence of models for spec 2020-05-07 17:36:52 +02:00
Drew Banin
784ab79eec Merge pull request #2402 from fishtown-analytics/feature/bigquery-column-comments
Feature/bigquery column comments
2020-05-07 10:55:09 -04:00
Drew Banin
7122a3f90b Add column comments on BigQuery for views, tables, and incrementals 2020-05-07 09:48:18 -04:00
Mikael Ene
868a447c58 alias for schema tests macros 2020-05-07 07:38:21 +02:00
Azhar Dewji
002244e3b3 Fixed CHANGELOG links and added contributors section 2020-05-06 21:41:47 -04:00
Azhar Dewji
5a319d6ee1 Updated CHANGELOG.md 2020-05-06 17:21:51 -04:00
Azhar Dewji
a1ca9025e7 Fix error when trying to call list_schemas macro for BigQuery adapter 2020-05-06 17:10:04 -04:00
Jacob Beck
9cd7cbc9e3 Merge pull request #2409 from fishtown-analytics/fix/plugin-version-files
update __version__.py files, add them to .bumpversion.cfg (#2408)
2020-05-06 10:19:03 -06:00
Jacob Beck
2d3dc1f6cc update __version__.py files, add them to .bumpversion.cfg 2020-05-06 09:41:10 -06:00
Drew Banin
d01c1614a5 Merge pull request #2407 from fishtown-analytics/fix/dupe-resource-type-error
Fix for error in error handler for duped data tests
2020-05-06 11:00:19 -04:00
Drew Banin
fb5320f7bc (#2406) Fix for error in error handler for duped data tests 2020-05-06 10:10:46 -04:00
Jacob Beck
7c916e9bdb Merge pull request #2321 from snowflakeseitz/column_comments
Column comments
2020-05-05 12:23:02 -06:00
Andrew Seitz
b774702ee8 removing duplicates in common 2020-05-05 10:21:36 -07:00
Andrew Seitz
ffaaacc148 adding in changelog 2020-05-05 10:05:14 -07:00
Jacob Beck
9de9335554 Merge pull request #2400 from fishtown-analytics/fix/disabling-tracking-resets-invocation-id
Fix: disabling tracking resets invocation ID (#2398)
2020-05-05 10:25:46 -06:00
Drew Banin
feae21992a Merge pull request #2399 from fishtown-analytics/docs/0.17.0
Update docs site for 0.17.0
2020-05-05 12:23:19 -04:00
Jacob Beck
18cfe81e00 use is not instead of != on time comparison check 2020-05-05 09:41:46 -06:00
Jacob Beck
869bdc454d Disabling tracking no longer resets the invocation ID
- re-use the same active user
- add disable_tracking method that does not reset invocation ID
2020-05-05 09:31:46 -06:00
Drew Banin
6725234466 Fix missing source columns in docs site 2020-05-05 11:08:47 -04:00
Drew Banin
05bf1512f9 (#2284) bump docs build 2020-05-05 09:07:21 -04:00
Andrew Seitz
bd2d1f7722 fixing failing test on incremental 2020-05-04 14:49:05 -07:00
Andrew Seitz
f58bc49176 fixing default 2020-05-04 14:49:05 -07:00
Andrew Seitz
5cabafc257 fixing rebase error 2020-05-04 14:49:05 -07:00
Andrew Seitz
0fdff04fc6 fixing rebase 2020-05-04 14:48:55 -07:00
Andrew Seitz
1448d0b0c2 adding in support for views 2020-05-04 14:48:05 -07:00
Andrew Seitz
f843e658a1 fixing spacing 2020-05-04 14:48:05 -07:00
Andrew Seitz
06ffb6dd53 adding in Drews feedback 2020-05-04 14:48:05 -07:00
Andrew Seitz
78110e17e5 basics to get column comments working 2020-05-04 14:48:05 -07:00
Andrew Seitz
74df1a1c31 adding persist docs tests 2020-05-04 14:48:05 -07:00
Andrew Seitz
d9862af8e1 adding in support for views 2020-05-04 14:47:46 -07:00
Andrew Seitz
ea82d8f8ca fixing spacing 2020-05-04 14:47:46 -07:00
Andrew Seitz
8de3235a63 adding in Drews feedback 2020-05-04 14:47:43 -07:00
Andrew Seitz
eb3ed5744b basics to get column comments working 2020-05-04 14:47:19 -07:00
Andrew Seitz
b98a4d82c3 rebasing 2020-05-04 14:42:47 -07:00
Jacob Beck
ac562d9388 Add dbt:0.17.0b1 dockerfiles and requirements 2020-05-04 15:09:43 -06:00
Jacob Beck
d0cb960455 Bump version: 0.17.0a1 → 0.17.0b1 2020-05-04 14:59:39 -06:00
Jacob Beck
7db16f8521 Merge pull request #2395 from fishtown-analytics/fix/bigquery-numbery-datasets
Add a special as_text filter to the native env (#2384)
2020-05-04 14:50:01 -06:00
Jacob Beck
229d3986c2 PR feedback: text filters for everyone 2020-05-04 14:17:13 -06:00
Jacob Beck
c237c8ee9e Merge pull request #2393 from fishtown-analytics/fix/suppress-bigquery-job-sql
Suppress the bigquery job sql in exception messages (#2383)
2020-05-04 13:49:22 -06:00
Jacob Beck
bd9a1320fa Merge pull request #2349 from franloza/fix/2173
Use original path to mimick resource subdirectory structure
2020-05-04 13:46:11 -06:00
Jacob Beck
9eb4506117 Merge branch 'dev/octavius-catto' into fix/2173 2020-05-04 13:45:25 -06:00
Jacob Beck
58c95bb399 Merge pull request #2389 from sethwoodworth/patch-1
Adds missing comma to `dbt compile` help text
2020-05-04 13:43:57 -06:00
Jacob Beck
af5af829cf add missing as_text to sources tests 2020-05-04 13:41:45 -06:00
Jacob Beck
0961bdb506 Add a special as_text filter to the native env that tells dbt to render the value as a string no matter what.
Use like "{{ 1991 | as_text }}" -> "1991"
Also, moved the weird special quoting handling logic out and instead avoid calling the renderer on column_name arguments
2020-05-04 13:18:49 -06:00
Jacob Beck
7b9111d386 Merge branch 'dev/octavius-catto' into patch-1 2020-05-04 10:37:39 -06:00
Jacob Beck
f8580a23ff Suppress the bigquery job sql in exception messages 2020-05-04 10:35:50 -06:00
Drew Banin
b5f99881fe Merge pull request #2390 from fishtown-analytics/fix/snapshot-staging-is-not-atomic
Fix/snapshot staging is not atomic
2020-05-04 12:24:49 -04:00
Seth Woodworth
6266216fda Merge branch 'dev/octavius-catto' into patch-1 2020-05-04 12:22:43 -04:00
Seth Woodworth
ca39ac5f00 Update changelog with PR #2389 2020-05-04 12:21:25 -04:00
Drew Banin
5d99798c7d Merge branch 'dev/octavius-catto' into fix/snapshot-staging-is-not-atomic 2020-05-04 11:00:29 -04:00
Drew Banin
24ed84dddd Merge pull request #2391 from fishtown-analytics/fix/snapshot-use-updated-at-timestamp-for-comparisons
Fix for changing snapshot strategy types
2020-05-04 10:39:23 -04:00
Drew Banin
e82e68d22a update changelog 2020-05-04 10:39:00 -04:00
Jacob Beck
e6bb06030c Merge pull request #2378 from fishtown-analytics/feature/pg-rs-persist-docs
Column and table comments for postgres/redshift (#2333)
2020-05-04 07:47:23 -06:00
Jacob Beck
595c82c1f1 Merge pull request #2387 from fishtown-analytics/fix/ambiguous-alias-exception
fix the ambiguous alias exception to respect databases
2020-05-04 07:18:55 -06:00
Jacob Beck
a46855821b Merge branch 'dev/octavius-catto' into fix/ambiguous-alias-exception 2020-05-04 07:18:27 -06:00
Drew Banin
89ba3e707a Tweak code comment 2020-05-04 09:00:20 -04:00
Drew Banin
2cd6a2d7a0 (#2350) Fix for changing snapshot strategy types 2020-05-02 12:38:57 -04:00
Drew Banin
1f758f90d0 Merge pull request #2376 from fishtown-analytics/fix/handle-query-array-types
Fix/handle query array types
2020-05-02 10:00:14 -04:00
Drew Banin
8681dd8c93 (#1884) Fix for non-atomic snapshot staging tables 2020-05-02 09:28:31 -04:00
Drew Banin
0781cef8b1 (#2337) Handle array values in agate dataframe building 2020-05-02 09:23:03 -04:00
Seth Woodworth
5edbe5758e Adds missing comma to dbt compile help text 2020-05-01 13:50:07 -04:00
Jacob Beck
20c5c4c3dd fix the ambiguous alias exception to respect databases
The alias name check is now tied to the behavior of adapter.Relation.create_from(...)
Plugins that override the `include` of their relations will use whatever they render to for the check
the actual exception-raising code gets the name that was compared instead of generating its own
Finally, I added a reasonable fallback behavior since this method was exposed to the context
2020-05-01 10:43:38 -06:00
Jacob Beck
e376c14e8a Merge branch 'dev/octavius-catto' into fix/2173 2020-05-01 08:45:08 -06:00
Drew Banin
68babfb4bb Merge pull request #2200 from fishtown-analytics/fix/sql-header-in-incrementals
Fix missing sql_header for incremental models
2020-05-01 10:11:59 -04:00
Drew Banin
f3d4377fdd Merge branch 'dev/octavius-catto' into fix/sql-header-in-incrementals 2020-05-01 10:10:21 -04:00
Fran Lozano
efaeb6786e Merge remote-tracking branch 'upstream/dev/octavius-catto' into fix/2173 2020-05-01 12:32:44 +02:00
Fran Lozano
96d2978a23 Update CHANGELOG.md 2020-05-01 12:29:37 +02:00
Jacob Beck
6e1665d1fb Merge pull request #2382 from fishtown-analytics/fix/remove-bq-location
remove location and information_schema.schematas query from catalog (#2320)
2020-04-30 18:47:08 -06:00
Jacob Beck
c6603be194 Handle funky incremental behavior in case existing materializations use it
- also fixed the default incremental to include that it is creating a table
2020-04-30 14:49:34 -06:00
Jacob Beck
d17e706351 improve parsing errors, include line:char instead of absolute character index 2020-04-30 14:49:34 -06:00
Jacob Beck
acd978e054 Column and table comments
- add some table comment framework stuff
- have redshift/postgres catalogs include table comments
- have redshift/postgres add comments to columns/tables/views
- push some bigquery-specific formatting into bigquery
- add tests for table comments
2020-04-30 14:49:34 -06:00
Jacob Beck
ed6591e450 remove location and information_schema.schematas query from catalog 2020-04-30 13:52:13 -06:00
Fran Lozano
fc61869ea8 Revert "Fix wrong path in test"
This reverts commit a3e6518c41.
2020-04-30 01:24:43 +02:00
Fran Lozano
80501e7f8f Fix wrong path in test 2020-04-30 01:19:09 +02:00
Fran Lozano
a3e6518c41 Fix wrong path in test 2020-04-30 00:58:19 +02:00
Fran Lozano
b71496a25d Fix wrong paths in tests 2020-04-30 00:45:55 +02:00
Fran Lozano
391ef70e67 Change build paths in docs generate tests according to new path format 2020-04-30 00:21:20 +02:00
Drew Banin
29e2bbc0c2 Merge branch 'dev/octavius-catto' into fix/sql-header-in-incrementals 2020-04-29 15:52:44 -04:00
Jacob Beck
5884f7df55 Merge pull request #2357 from fishtown-analytics/feature/schema-yml-rendering-3
Schema yaml rendering and source overrides
2020-04-29 13:52:22 -06:00
Jacob Beck
6708046951 Merge pull request #2341 from dcereijodo/log-depends-on-extra
log 'depends_on_nodes' node attribute in the log record 'extra' field
2020-04-29 07:13:33 -06:00
Jacob Beck
4751a5f3e2 Merge pull request #2361 from Fokko/patch-1
Add python_requires to setup.py
2020-04-28 13:17:30 -06:00
Jacob Beck
0b82def5f5 support windows in source overrides tests 2020-04-28 12:49:49 -06:00
Jacob Beck
ee529a5472 fix catalog unit tests 2020-04-28 11:52:03 -06:00
Jacob Beck
d38344125e move catalog nodes entry into sources
Fix tests accordingly
2020-04-28 11:37:00 -06:00
Jacob Beck
caee9415e4 PR feedback
- add paths to source patches, apply them to parsed source definitions
- warn on unused source patches
- fix error on duplicate source patches
- add sources back into the catalog
- update changelog
2020-04-28 11:37:00 -06:00
Jacob Beck
7e85ad95c7 Add some very special quoting rules for rendering
Handle quoting in render() instead of get_rendered code
 - avoid stripping the quotes from a string that was returned as jinja
2020-04-28 11:29:06 -06:00
Jacob Beck
86a35d6c13 Add file paths to dbt_project.yml when it fails to render 2020-04-28 11:29:06 -06:00
Jacob Beck
ff3360afc0 flip the flag
Add/modify tests to test native rendering/config application
2020-04-28 11:29:06 -06:00
Jacob Beck
eea322e647 update changelog 2020-04-28 11:29:05 -06:00
Jacob Beck
120f0f0959 Tests
add override tests
fix some unit tests
2020-04-28 11:28:48 -06:00
Jacob Beck
38443cf3f5 implement source patching 2020-04-28 11:28:48 -06:00
Jacob Beck
c69f28e0a0 move sources into their own dict 2020-04-28 11:28:48 -06:00
Jacob Beck
704e44e479 add source patches, distinguished by override, store them on run results/the manifest 2020-04-28 11:28:48 -06:00
Jacob Beck
c19a7d7b32 Docs now get the schema.yml parser vars
The SchemaParser now renders the entire schema.yml except tests/descriptions
Snapshot config resolution
2020-04-28 11:28:48 -06:00
Jacob Beck
7323ddbabf Give sources a sane FQN, initial schema var rendering stuff 2020-04-28 11:28:48 -06:00
Jacob Beck
0b7dc326e9 remove docs parse context - not really used 2020-04-28 11:28:48 -06:00
Jacob Beck
afc0341b54 Merge pull request #2363 from fishtown-analytics/fix/vars-invalidation
Vars invalidation (#2265)
2020-04-28 10:06:22 -06:00
Jacob Beck
0df8462562 Merge branch 'dev/octavius-catto' into fix/vars-invalidation 2020-04-28 08:36:47 -06:00
David
305ace528a Merge branch 'dev/octavius-catto' into log-depends-on-extra 2020-04-28 09:28:45 +02:00
Drew Banin
f7b8e5c861 Merge pull request #2348 from fishtown-analytics/fix/decimal-to-json-bq
Fix for non-json-serializable values in BigQuery nested columns
2020-04-27 15:22:16 -04:00
Jacob Beck
8bf5dd0502 Merge pull request #2364 from fishtown-analytics/feature/webbrowser-no-deadlock
add --no-browser argument to docs serve
2020-04-27 12:04:19 -06:00
Fokko Driesprong
8d84da40e1 Add python_requires to setup.py
This will let people know that they are running
a too old version of Python

Signed-off-by: Fokko Driesprong <fokko@apache.org>
2020-04-27 19:35:05 +02:00
Jacob Beck
ab886cde16 added --no-browser argument to docs serve 2020-04-27 11:09:20 -06:00
Jacob Beck
ab5432daa0 add a test for vars that change manifest-compile behavior 2020-04-27 10:31:21 -06:00
Jacob Beck
60d4708f15 refactor test suite 2020-04-27 10:31:21 -06:00
Jacob Beck
7fba68fcbd invalidate the manifest when vars change 2020-04-27 10:31:21 -06:00
Fran Lozano
8f45ecebb4 Consider many-to-one relationships from nodes to files 2020-04-26 21:43:01 +02:00
Christian Kohlberg
c51ba50946 Expose Snowflake query id in case of an exception raised by the connector
* Allows to search for the query id inside the Snowflake query history
* Allows for better communication with snowflake support through query ids
2020-04-26 13:28:08 +02:00
Jacob Beck
616b32eb67 Merge pull request #2312 from fishtown-analytics/feature/dbt-project-v2
dbt project v2
2020-04-22 07:40:57 -06:00
Fran Lozano
278f764d61 Use original path to mimick resource subdirectory structure 2020-04-22 01:18:18 +02:00
Jacob Beck
5ed5693cd5 remove warning for "vars" in config blocks 2020-04-21 16:43:14 -06:00
Jacob Beck
236a087a42 skip deprecation warnings on non-graph runnable tasks so the deprecation warnings on graph runnable tasks are better 2020-04-21 15:10:17 -06:00
Drew Banin
044f8fce4e Update CHANGELOG.md
Co-Authored-By: Jacob Beck <beckjake@users.noreply.github.com>
2020-04-21 16:19:38 -04:00
Jacob Beck
618adf501d PR feedback: adjust deprecation warning copy 2020-04-21 13:46:39 -06:00
Jacob Beck
0da3b07fb3 add type annotations, add some unit tests for MultiDict 2020-04-21 13:15:59 -06:00
Drew Banin
4bea303c5f (#2336) Fix for non-json-serializable values in BigQuery nested columns 2020-04-21 15:00:04 -04:00
Jacob Beck
c18b72b53d PR feedback
Handle empty vars dict
Check sources for unused configs
  - add a test
Warn when dbt finds "vars" in "models", "seeds", etc blocks
  - add a test
Clean up enabled/disabled code to share between sources and nodes
  - only log downstream tests at debug level when a source is disabled
  - include the offending project name in the v1 deprecation message
  - Fix tests that care about error messages, add new ones
2020-04-21 12:41:19 -06:00
dcereijodo
ac9ea53e8e updated changelog and contributors 2020-04-21 08:24:07 +02:00
Jacob Beck
669a18564f instead of a special "config" key, use a "+" prefix and only require it on dict values
Make the package-duplicates test use a local dependency
2020-04-20 14:30:44 -06:00
Jacob Beck
c6b234673c deprecate v1 configs
Bump included projects to v2
fix tests
2020-04-20 11:56:14 -06:00
Jacob Beck
1adf994740 Convert tests to v2, fix them
Also add some missing agate methods to stubs
fix tests from splitting the config renderer
2020-04-20 11:56:14 -06:00
Jacob Beck
72d82e45ed Fix some mypy issues
Split up hooks to expose the hook dict
Remove some odd imports
2020-04-20 11:56:14 -06:00
Jacob Beck
070e13ded8 Split ConfigRenderer into parts
Docs now get the schema.yml parser vars
The SchemaParser now renders the entire schema.yml except tests/descriptions
2020-04-20 11:56:14 -06:00
Jacob Beck
c7d8031681 Support for v1/v2 config blocks
- Backwards compatiblity shims from v2 to v1 configs
- config merging for v2
- compatibility shim for parsing/contexts
- defer var lookups as late as possible
- fixed ContextConfigType to be a proper Union of the two context config types
- Fix adapter configs to be proper dataclasses
- fix unused config paths error
- make v2 config parsing not alter its source data
2020-04-20 11:31:00 -06:00
Jacob Beck
bf8932efaa Make node configs work like they should, clean up the json schema/parsing for snapshot configs 2020-04-20 11:30:59 -06:00
Jacob Beck
ff9e0cd4cf Add get_config_class_by_name function for adapters 2020-04-20 11:30:59 -06:00
Jacob Beck
1522d4cb2b Move configs into their own graph module
Configs all get defaults, a base class
Added SourceConfig
Introduced the idea of MergeBehavior and ShowBehavior
Implemented updates/merges based on MergeBehavior for configs
Implemented typed adapter-specific configs and behavior
2020-04-20 11:30:59 -06:00
Jacob Beck
fac8a286e8 Split SourceConfig into LegacyContextConfig and ConfigUpdater
Renamed config.config to config.build_config_dict()
mypy annotations
rename a module to avoid duplicating names
2020-04-20 11:18:29 -06:00
Jacob Beck
d37e74c62c Rework vars to support v1 and v2
- Rename project contract to projectv1
- Add load_dependencies and dependencies attrs to RuntimeConfig
  - Moved project loading from parser/manifest.py into methods of RuntimeConfig
- Move methods that check for unused configs from Project to RuntimeConfig
  - they require the adapter type to be correct
- Rework Var in provider vs base context to handle both styles of Var configuration
2020-04-20 11:18:28 -06:00
Jacob Beck
0a1862e92e refactor source config 2020-04-20 11:18:28 -06:00
dcereijodo
9dd16045fb log 'depends_on_nodes' node attribute in the log record 'extra' field 2020-04-18 11:11:49 +02:00
Jacob Beck
c05b45b241 Merge pull request #2339 from nickwu241/fix/0.16.1/UnsetProfileConfig-use-args-project-dir
Update UnsetProfileConfig.from_args to use args.project_dir if passed
2020-04-17 14:59:24 -06:00
Nick Wu
2a0f69ac5f Update test to change out of dir that will be deleted 2020-04-17 10:35:09 -07:00
Nick Wu
d5f50954c0 Add test for running simple dbt commands with --project-dir arg 2020-04-17 09:58:10 -07:00
Nicholas Wu
dda9289d77 Update core/dbt/config/runtime.py
Co-Authored-By: Jacob Beck <beckjake@users.noreply.github.com>
2020-04-17 07:55:56 -07:00
Nick Wu
66f9ee0b3e Updatet CHANGELOD.md with dbt deps fix 2020-04-17 03:22:28 -07:00
Nick Wu
72d2cab517 Update UnsetProfileConfig.from_args docstring 2020-04-17 02:40:45 -07:00
Nick Wu
a376d656a7 Update UnsetProfileConfig.from_args to use args.project_dir if passed 2020-04-17 02:30:30 -07:00
Jacob Beck
204fc25c21 Merge pull request #2291 from fishtown-analytics/feature/test-source-overrides-deps
Tests for source overriding (#2264)
2020-04-16 10:57:01 -06:00
Jacob Beck
bea8c72a82 Merge branch 'dev/octavius-catto' into feature/test-source-overrides-deps 2020-04-16 10:18:13 -06:00
Jacob Beck
16c1fcf4db Update CHANGELOG.md
Co-Authored-By: Drew Banin <drew@fishtownanalytics.com>
2020-04-16 10:17:12 -06:00
Jacob Beck
566f78a95c Merge pull request #2322 from fishtown-analytics/feature/show-objects-in-schema
use "show objects in schema" (#2174)
2020-04-16 07:26:31 -06:00
Jacob Beck
6ca707822e Merge branch 'dev/octavius-catto' into feature/show-objects-in-schema 2020-04-16 07:21:18 -06:00
Jacob Beck
59477a32bc Merge pull request #2324 from fishtown-analytics/feature/sf-describe-columns-in-table
Use "describe table" to get the columns in a relation on snowflake (#2260)
2020-04-16 07:19:12 -06:00
Jacob Beck
bc38750d47 PR feedback, tests 2020-04-15 10:11:30 -06:00
Jacob Beck
51278c0fbf Merge pull request #2325 from fishtown-analytics/fix/bigquery-include-vs-quote-policy
Fix include_policy vs. quote_policy bug in BigQueryRelation (#2188)
2020-04-15 08:54:09 -06:00
Jacob Beck
644e3e8721 Fix #2188
The BigQuery information schema previously used its quote policy as the basis
for a new include policy, rather than its include policy.
2020-04-14 14:42:29 -06:00
Jacob Beck
107bc5c217 Use "describe table" to get the columns in a relation on snowflake 2020-04-14 14:02:27 -06:00
Jacob Beck
1313445afe change list_relations_without_caching to use "show objects in schema" 2020-04-14 12:33:04 -06:00
Jacob Beck
79db8807a6 Merge pull request #2319 from fishtown-analytics/feature/compile-ls-relation-caches
dbt compile and dbt ls relation caches (#1705)
2020-04-14 12:22:33 -06:00
Drew Banin
4dc12c72c2 Merge pull request #2310 from fishtown-analytics/fix/incorrect-skip-counts
Fix incorrect SKIP count in stdout
2020-04-14 13:51:26 -04:00
Jacob Beck
ac427bdc0c populate_adapter_cache for everyone
Added the populate_adapter_cache to the GraphRunnableTask before_run method
 - Previously only Run used it
2020-04-14 10:44:25 -06:00
Drew Banin
96a3736cd7 (#2095) fix incorrect SKIP count in stdout 2020-04-14 12:30:31 -04:00
Jacob Beck
6424b65097 Merge pull request #2318 from fishtown-analytics/fix/bq-dp-models-failure
Fix the tests (#2317)
2020-04-14 10:21:17 -06:00
Jacob Beck
c6121675bb re-add quoting logic 2020-04-14 08:07:00 -06:00
Jacob Beck
626f835601 Jinja removed a keyword argument
Since they apparently will do this in patch releases, pinnned to precisely 2.11.2
But, it means we don't have to override NativeSandboxTemplate.render
anymore, so that's nice
2020-04-14 07:27:27 -06:00
Jacob Beck
aa38c8101b Fix the tests
This looks like a neat interplay between yaml's anchor/pointer notation (it's
a reference, not a copy!), and dbt inserting 'model' into the keyword args for
tests.

I've added a `copy.deepcopy` call to where dbt collects test arguments from the
underlying file, so the 'model' is inserted into a fresh keyword arguments
dictionary.
2020-04-13 20:18:30 -06:00
Drew Banin
d2760d5a49 Merge pull request #2298 from rodrigodelmonte/fix-diststyle-auto
Change redshift diststyle behavior
2020-04-09 21:15:34 -04:00
Rodrigo Monte
0c3f9e7e2e Merge branch 'dev/octavius-catto' into fix-diststyle-auto 2020-04-09 22:57:34 +02:00
Jacob Beck
fd4a33e450 Fix merge: it doubled up a few lines 2020-04-08 12:03:19 -06:00
Jacob Beck
f07face7c0 Merge branch 'dev/0.16.1' into dev/octavius-catto 2020-04-08 11:47:47 -06:00
Jacob Beck
935f985e25 Merge pull request #2297 from sumanau7/fix/return_proper_msg_for_empty_profile
Return proper error message when empty profiles.yml is used for project
2020-04-08 10:44:49 -06:00
Sumanau Sareen
35b43e9708 Update changelog.md with github issue and PR details 2020-04-08 20:57:42 +05:30
Sumanau Sareen
9991b066c7 Check if profile data with given profile name is empty 2020-04-08 20:57:07 +05:30
Sumanau Sareen
c76d6f34a2 Return proper error message when empty profiles.yml is used for dbt project 2020-04-08 20:57:07 +05:30
Jacob Beck
380f7c2e51 Merge pull request #2263 from sumanau7/fix/read_filenames_using_case_insenstive_extension
Read filenames using case insensitive extension
2020-04-08 08:02:03 -06:00
rodrigodelmonte
ab07b8ca06 Change redshift diststyle behavior (#2246)
* Don't provide a diststyle to redshift tables when configured as "auto"
2020-04-07 22:34:04 +02:00
Sumanau Sareen
6f302e26b3 Update changelog.md with github issue and PR details 2020-04-07 22:49:50 +05:30
Sumanau Sareen
f9343c4683 1. Add integration test for yaml schema extension warning. 2. Add integration test for checking case-insensitive extensions 2020-04-07 22:08:11 +05:30
Sumanau Sareen
466ed1f8e6 Add warning in case core config files are found with yaml extension 2020-04-07 21:49:06 +05:30
Sumanau Sareen
83cdd40191 FilesystemSearcher now searches for file names ignoring the case of the extension 2020-04-07 21:49:06 +05:30
Drew Banin
579a6d6064 Update changelog 2020-04-06 20:35:48 -04:00
Drew Banin
3a5eb4c2ad Fix for bq insert_overwrite incrementals, add additional tests 2020-04-06 17:58:00 -04:00
Drew Banin
40455108bb Merge branch 'dev/octavius-catto' of github.com:fishtown-analytics/dbt into fix/sql-header-in-incrementals 2020-04-06 17:12:28 -04:00
Jacob Beck
2c241025c9 Merge pull request #2279 from sumanau7/add_dbt_plugin_version
Add dbt plugin version
2020-04-06 14:41:00 -06:00
Sumanau Sareen
f5697fd82b Update CHANGELOG.md
Co-Authored-By: Jacob Beck <beckjake@users.noreply.github.com>
2020-04-07 00:55:54 +05:30
Sumanau Sareen
10d20197ed Update changelog.md with github issue and PR details 2020-04-07 00:38:47 +05:30
Sumanau Sareen
5d5913efeb Add DBT Plugin information to get_version_information 2020-04-05 00:33:16 +05:30
Jacob Beck
8cc0178b64 Add dbt:0.16.1rc1 dockerfiles and requirements 2020-04-03 12:24:18 -06:00
Jacob Beck
6a26a5f47d Merge pull request #2258 from fishtown-analytics/feature/model-select-paths
Add a `path:` selector to the node selector (#454)
2020-04-03 12:23:23 -06:00
Jacob Beck
01f5ce73a3 Bump version: 0.16.1a1 → 0.16.1rc1 2020-04-03 12:13:45 -06:00
Jacob Beck
1e94b1f5bb Merge pull request #2293 from fishtown-analytics/fix/duplicate-plugin-macros
Fix an error in exception handling for duplicate macros (#2288)
2020-04-03 12:12:41 -06:00
Jacob Beck
13e8b0dd0d Merge branch 'dev/0.16.1' into fix/duplicate-plugin-macros 2020-04-03 12:12:02 -06:00
Jacob Beck
f25b830aad Merge pull request #2294 from fishtown-analytics/fix/repair-create-adapter-script.py
Update the create adapter script to dbt 0.16.1
2020-04-03 11:05:46 -06:00
Jacob Beck
be0e12edb0 Fix an error in exception handling
Add a unit test
2020-04-03 10:53:14 -06:00
Jacob Beck
6aafd827f9 Update the create adapter script to dbt 0.16.1
Make the base dbt version in the script managed by bumpversion
2020-04-03 10:29:05 -06:00
Jacob Beck
41746eebbb Tests for source overriding
- Make sure dependency sources can be selected from the root project
- Make sure the root project can override dependency sources
2020-04-02 10:34:11 -06:00
Jacob Beck
fb3019f3f1 Merge pull request #2290 from fishtown-analytics/fix/deps-no-profile-required
Do not require a profile in "dbt deps" (#2231)
2020-04-02 10:16:46 -06:00
Jacob Beck
aa3cb37e05 PR feedback: name change 2020-04-02 09:42:22 -06:00
Jacob Beck
f0a4810b08 Use config.credentials.type instead of get_adapter(config).type()
- remove import cycle
  - works even if adapter class has been loaded but adapter object has not been registered
2020-04-02 09:17:28 -06:00
Jacob Beck
42e8c56b13 in "dbt deps", if a profile cannot be found/loaded, use invalid values instead
Add tests
2020-04-02 08:42:16 -06:00
Jacob Beck
ff0e955ccb Merge pull request #2280 from fishtown-analytics/fix/add-partitions-bq-config
Add "partitions" to the adapter specific config (#2256)
2020-04-01 12:12:49 -06:00
Jacob Beck
f8ee0561cb PR feedback
Also, added more tests
2020-04-01 10:14:24 -06:00
Jacob Beck
6c7d71028b Add a path: selector to the node selector
Instead of always being FQN by default, path-like selectors default to PATH
Paths referring to non-root packages are ignored
Added a test to the dependency tests that checks both of these
2020-04-01 10:14:23 -06:00
Jacob Beck
c63f60ef7c Add "partitions" to the adapter specific config
And tests, of course
2020-04-01 10:06:42 -06:00
Jacob Beck
79e8a86750 Merge pull request #2281 from fishtown-analytics/fix/quoted-information-schema
do not double-quote the database name (#2267)
2020-04-01 10:03:44 -06:00
Jacob Beck
aa0e58ef3b Merge pull request #2262 from kyleabeauchamp/kab_1995
Add db_groups and autocreate flags to Redshift config
2020-04-01 09:53:18 -06:00
Jacob Beck
4c93b51427 Merge pull request #2248 from fishtown-analytics/feature/document-contexts
Add documentation for contexts and manifests, plus extraction scripts (#2202)
2020-04-01 09:30:22 -06:00
Kyle Beauchamp
f4f10f6dd2 Fixes to changelog 2020-04-01 07:58:25 -07:00
Jacob Beck
e9b537a861 Add documentation for contexts and manifests, plus extraction scripts
Contexts are generally documented based on the dbt documentation page
Manifest is documented using the 'description' metadata field that hologram supports
Added two scripts:
 - one to extract context metadata
 - one to extract the manifest/run result/catalog json schemas
 - both scripts run during circleCI and dump the json files to disk
   - which are then uploaded as build artifacts on circle
2020-04-01 08:46:01 -06:00
Jacob Beck
b385d176a7 Merge pull request #2251 from fishtown-analytics/fix/render-profile-name-base-context
Render profile_name in the base context (#2230)
2020-04-01 08:40:23 -06:00
Jacob Beck
df2ae076b9 Merge branch 'dev/0.16.1' into fix/render-profile-name-base-context 2020-04-01 08:39:19 -06:00
Jacob Beck
9e6e381a40 Merge pull request #2282 from fishtown-analytics/feature/append-query-comment-backport
append query comment backport (#2138)
2020-04-01 08:37:55 -06:00
Jacob Beck
a8bdaba497 Merge branch 'dev/0.16.1' into feature/append-query-comment-backport 2020-04-01 08:37:26 -06:00
Jacob Beck
e3ac64d14a PR feedback 2020-04-01 08:37:07 -06:00
Jacob Beck
c146e5076e Merge pull request #2252 from fishtown-analytics/fix/docs-blocks-embedding
Make the contents of the raw blocks regex pattern non-greedy (#2241)
2020-04-01 08:35:24 -06:00
Jacob Beck
374d0b4e91 Merge branch 'dev/0.16.1' into fix/docs-blocks-embedding 2020-04-01 08:34:56 -06:00
Jacob Beck
c415b6b41f Merge pull request #2237 from fishtown-analytics/feature/slim-manifest
slim the manifest
2020-04-01 08:33:48 -06:00
Jacob Beck
3807b0b6cd CHANGELOG update 2020-03-31 15:14:16 -06:00
ilkin Balkanay
8b05113cc8 pass integration test
- to disable query comments use : `'query-comment': ''`
2020-03-31 15:05:10 -06:00
ilkin Balkanay
304b11ee09 NoValue type definition updated.
- hologram version updated to 0.0.6 to be able to use QueryComment, NoValue and str within the same Union type.
2020-03-31 15:05:10 -06:00
ilkin Balkanay
11b7bf77c0 updated changelog
- added ilkinulas to contributers list
2020-03-31 15:05:10 -06:00
ilkin Balkanay
ee2d990ece updated change log.
- replaced pr link with issue link
2020-03-31 15:05:10 -06:00
ilkin Balkanay
031d844d30 refactored _get_comment_macro method
- isinstance check is needed to pass mypy checks
2020-03-31 15:05:10 -06:00
ilkin Balkanay
33d3c9f0bd pass unit test 2020-03-31 15:05:10 -06:00
ilkin Balkanay
a6aa1c36b3 refactored types (pass mypy checks) 2020-03-31 15:05:10 -06:00
ilkin Balkanay
90542fb499 added QueryComment dataclass 2020-03-31 15:05:10 -06:00
ilkin Balkanay
70031c63b6 updated CHANGELOG 2020-03-31 15:05:09 -06:00
ilkin Balkanay
b781cdad6d fix integration test. 2020-03-31 15:05:09 -06:00
ilkin Balkanay
504c163570 fixed unittest: default value for query_comment is None 2020-03-31 15:05:09 -06:00
ilkin Balkanay
5f294bc587 handle unset query comment 2020-03-31 15:05:09 -06:00
ilkin Balkanay
b202da7d84 return default comment if config.query_comment is None 2020-03-31 15:05:09 -06:00
ilkin Balkanay
43525d622a fixed mypy error 2020-03-31 15:05:09 -06:00
ilkin Balkanay
0979c67b63 added unit test for query_headers.py 2020-03-31 15:05:09 -06:00
ilkin Balkanay
90abe7bbe7 pass unit tests 2020-03-31 15:05:09 -06:00
ilkin Balkanay
9c9d6248ae added query comment append feature (wip) 2020-03-31 15:05:09 -06:00
Jacob Beck
8c7fef47a4 do not double-quote the database name 2020-03-31 14:57:31 -06:00
Jacob Beck
78f1afa959 Merge pull request #2259 from jeremyyeo/fix/warn-exception-sql-compilation
Make warn return empty string (#2222)
2020-03-30 08:10:21 -06:00
Jeremy Yeo
f5d358d3cf add to changelog 2020-03-28 10:58:56 +13:00
Jeremy Yeo
55cd72cf62 add test 2020-03-28 10:23:30 +13:00
Jeremy Yeo
11d62f6c13 make warn return empty string 2020-03-28 09:57:37 +13:00
Jacob Beck
4182c03258 Merge pull request #2255 from fishtown-analytics/fix/all-null-seed-column-types
Fix a ValueError on redshift when an all-null column had a text type …
2020-03-27 11:44:44 -06:00
Jacob Beck
2da96e3169 Fix a ValueError on redshift when an all-null column had a text type specified
add unit tests for conversions
2020-03-27 09:40:27 -06:00
Jacob Beck
8e3c95b48c Make the contents of the raw blocks regex pattern non-greedy
This fixes an issue where a file with multiple raw blocks got treated as one _big_ one
 - it extended from the first {% raw %} to the last {% endraw %}
2020-03-26 13:36:45 -06:00
Jacob Beck
3c3f57ece1 fix unit tests 2020-03-26 13:03:43 -06:00
Jacob Beck
69ec5fd82e Render profile_name in the base context 2020-03-26 12:44:06 -06:00
Jacob Beck
386895f800 Merge pull request #2249 from fishtown-analytics/fix/rev-google-lower-bound
rev the bigquery minimum versions to a big number (#2233)
2020-03-26 11:42:38 -06:00
Jacob Beck
6065237de4 fix unit test bug that only appears on final release versions 2020-03-26 10:58:57 -06:00
Jacob Beck
a7617745e2 rev the bigquery minimum versions to a big number 2020-03-26 10:57:05 -06:00
Jacob Beck
923f28225a Bump version: 0.16.0 → 0.16.1a1 2020-03-26 10:57:05 -06:00
Jacob Beck
7dfef27886 Slim the manifest
remove raw_sql from ParsedMacros, remove file_contents from docs
get rid of seed_file_path
Do not write 'manifest.files' to disk
2020-03-26 10:29:56 -06:00
Jacob Beck
5c698908a4 Merge pull request #2244 from fishtown-analytics/feature/source-aliases
Add support for source aliases (#2133)
2020-03-26 08:12:17 -06:00
Jacob Beck
d88089fb24 Merge branch 'dev/octavius-catto' into feature/source-aliases 2020-03-25 14:52:11 -06:00
Jacob Beck
0188d03d1b Merge pull request #2238 from fishtown-analytics/fix/macro-kwarg-error-add-file
fix commit messages by using the stack properly (#2073)
2020-03-25 14:41:10 -06:00
Jacob Beck
7b6ea338ed Merge pull request #2220 from fishtown-analytics/feature/schema-tests-pass-rendered
In schema tests, pass rendered items (#2149)
2020-03-25 14:40:57 -06:00
Jacob Beck
40e88ded25 Merge pull request #2245 from fishtown-analytics/fix/circleci-pipelines
set the circleci config version to 2.1 (#2207)
2020-03-25 14:03:33 -06:00
Jacob Beck
38bcc2b736 override NativeSandboxTemplate.render(), remove the quotes hack 2020-03-25 14:03:17 -06:00
Jacob Beck
73e6941817 circleci 2.1 2020-03-25 13:32:56 -06:00
Jacob Beck
1268c119b5 require jinja 2.11.x 2020-03-25 10:46:45 -06:00
Jacob Beck
434edce8a7 remove extra methods 2020-03-25 10:46:44 -06:00
Jacob Beck
c0ceaee122 Add support for source aliases 2020-03-25 10:43:11 -06:00
Jacob Beck
e9a053c18d Update core/dbt/node_runners.py
Co-Authored-By: Drew Banin <drew@fishtownanalytics.com>
2020-03-25 07:30:02 -06:00
Jacob Beck
427f3bf163 fix commit messages by using the stack properly 2020-03-24 15:12:52 -06:00
Jacob Beck
96cfc49cb9 rev hologram to 0.0.7 2020-03-24 09:13:00 -06:00
Jacob Beck
dc65118f17 Add metadata to tests, add a native env
Render test arguments in the native env and pass them along to the context
added/fixed tests
Update changelog
2020-03-24 09:11:24 -06:00
Jacob Beck
31025a0a7f remove wrapped_sql, move data test wrapping into the node runner 2020-03-24 09:09:49 -06:00
Jacob Beck
d20c301dcd split test types into data/schema tests 2020-03-24 09:09:49 -06:00
Jacob Beck
0a382c411f Merge branch 'dev/barbara-gittings' into dev/octavius-catto 2020-03-24 09:09:13 -06:00
Jacob Beck
367bf6849b Merge pull request #2228 from Raalsky/bug/registry-timeout
(#2195) Added timeout to registry download call
2020-03-24 08:58:11 -06:00
Jacob Beck
848716607c Merge pull request #2224 from Raalsky/fail-fast
Add --fail-fast argument for dbt run and dbt test
2020-03-24 08:36:45 -06:00
Jacob Beck
0a0e71a9ff Merge branch 'dev/octavius-catto' into fail-fast 2020-03-24 08:13:24 -06:00
Jacob Beck
ca232dbdf4 Merge branch 'feature/rpc-get-manifest' into dev/octavius-catto 2020-03-24 07:22:58 -06:00
Jacob Beck
efcf78b348 Merge pull request #2199 from ilkinulas/query_comment_append
Append query comment
2020-03-24 06:51:14 -06:00
Jacob Beck
bbc0d30fbf When a user calls the 'get-manifest' API call, compile all the full project and return the compiled manifest from memory.
'get-manifest' returns an async ID, just like the existing compile/run/etc
2020-03-23 12:59:36 -06:00
Raalsky
1c60882525 (#2195) DBT_HTTP_TIMEOUT moved inside system download call 2020-03-23 17:56:37 +01:00
Raalsky
d6cb415f76 (#2195) DBT_HTTP_TIMEOUT environment variable added 2020-03-23 17:48:54 +01:00
Jacob Beck
f58bf878d5 Add dbt:0.16.0 dockerfiles and requirements 2020-03-23 08:20:20 -06:00
Jacob Beck
2fbc716615 Bump version: 0.16.0rc4 → 0.16.0 2020-03-23 07:59:37 -06:00
Raalsky
cce99b1ff2 (#2195) Added entry to CHANGELOG 2020-03-23 14:14:21 +01:00
Raalsky
f98b1669fa (#2195) Added timeout to registry download call 2020-03-23 13:35:31 +01:00
Raalsky
efebabe44f 058_fail_fast test method renamed 2020-03-22 18:39:43 +01:00
Rafal Jankowski
42a0461f57 Added integration test 058_fail_fast 2020-03-22 18:27:55 +01:00
Rafal Jankowski
a23ad3ad4a Linker some_task_done method lock refactor to be more consistent 2020-03-22 18:27:47 +01:00
Rafal Jankowski
1478be716d FailFastException refactor to RuntimeException 2020-03-22 18:27:32 +01:00
ilkin Balkanay
0eed51dfdd pass integration test
- to disable query comments use : `'query-comment': ''`
2020-03-21 21:36:45 +03:00
İlkin Balkanay
574d65ab4b Merge branch 'dev/octavius-catto' into query_comment_append 2020-03-21 17:31:30 +03:00
ilkin Balkanay
ac336b20ca NoValue type definition updated.
- hologram version updated to 0.0.6 to be able to use QueryComment, NoValue and str within the same Union type.
2020-03-21 17:21:24 +03:00
Rafal Jankowski
c9613016e2 Added --fail-fast argument for dbt run and dbt test 2020-03-20 19:26:17 +00:00
Jacob Beck
f9696a85d3 Merge pull request #2226 from fishtown-analytics/fix/add-docker-stuff-unlink-dbt
Build script updates - add docker stuff, unlink dbt (#2211)
2020-03-20 13:16:26 -06:00
Jacob Beck
deb97546c0 add 0.16.0rc4 dockerfiles and requirements 2020-03-20 12:16:05 -06:00
Jacob Beck
cea964910d Also unlink "dbt" if it is installed when test-installing the new homebrew package 2020-03-20 12:11:54 -06:00
Jacob Beck
f8b99f2e34 Bump version: 0.16.0rc3 → 0.16.0rc4 2020-03-20 11:31:17 -06:00
Jacob Beck
3a7e328c6e Update changelog 2020-03-20 11:31:01 -06:00
Jacob Beck
732792fc80 Merge pull request #2225 from fishtown-analytics/fix/build-script
Do not run brew audit on the default formula, it complains
2020-03-20 11:30:18 -06:00
Jacob Beck
dd842ac3b8 Do not run brew audit on the default formula, it complains about versions 2020-03-20 11:15:11 -06:00
Jacob Beck
bdcf10e209 Merge branch 'dev/barbara-gittings' into dev/octavius-catto 2020-03-20 11:05:55 -06:00
Jacob Beck
a43edd9dd8 Merge pull request #2223 from dmateusp/add_dockerize
Add Dockerize to test image
2020-03-20 09:34:00 -06:00
ilkin Balkanay
65d9c187b8 updated changelog
- added ilkinulas to contributers list
2020-03-20 00:04:14 +03:00
ilkin Balkanay
75e1c1dac8 updated change log.
- replaced pr link with issue link
2020-03-19 23:47:06 +03:00
Daniel Mateus Pires
28ccdcc52d Add Dockerize 2020-03-19 20:36:32 +00:00
ilkin Balkanay
7c34ff166a refactored _get_comment_macro method
- isinstance check is needed to pass mypy checks
2020-03-19 17:48:26 +03:00
ilkin Balkanay
36bf479115 pass unit test 2020-03-19 14:10:19 +03:00
ilkin Balkanay
3b90bfcdc3 refactored types (pass mypy checks) 2020-03-19 11:30:32 +03:00
ilkin Balkanay
ab19135ca6 added QueryComment dataclass 2020-03-19 10:25:29 +03:00
Jacob Beck
41f7b8cb9d Merge pull request #2215 from fishtown-analytics/feature/add-spark-deps
add spark deps to the test container
2020-03-18 18:05:00 -06:00
Jacob Beck
d8b9541b86 Merge pull request #2214 from fishtown-analytics/fix/bigquery-version
increase the google-cloud-bigquery lower bound to ensure we support "range_partitioning"
2020-03-18 15:53:57 -06:00
Jacob Beck
a4620adbce add spark deps 2020-03-18 15:52:23 -06:00
Jacob Beck
02ac87d160 increase the lower bound to ensure we support "range_partitioning" 2020-03-18 15:20:50 -06:00
Jacob Beck
f4c62726ee Merge pull request #2198 from fishtown-analytics/rework/incremental-overwrite
Rework insert_overwrite incremental strategy
2020-03-18 13:11:06 -06:00
Jacob Beck
0844be5a28 Merge pull request #2208 from fishtown-analytics/fix/bigquery-numeric-catalog
Fix issue with table databases/schemas/names becoming numbers (#2206)
2020-03-17 14:41:35 -06:00
Jacob Beck
3a77626cd7 Fix issue with table databases/schemas/names not being interpreted as strings if they look like numbers 2020-03-17 13:07:22 -06:00
Jeremy Cohen
4fee33c020 Unquote partition values in predicate 2020-03-16 13:01:27 -04:00
ilkin Balkanay
e3c53ee874 updated CHANGELOG 2020-03-14 13:55:02 +03:00
Drew Banin
367eff77fc (#2136) Fix missing sql_header for incremental models 2020-03-12 15:56:43 -04:00
Jeremy Cohen
1bed6a1b96 Rename dbt_partitions_for_upsert to dbt_partitions_for_replacement 2020-03-12 10:51:09 -04:00
ilkin Balkanay
1cf9220380 fix integration test. 2020-03-12 17:18:34 +03:00
ilkin Balkanay
fd8629a6cf fixed unittest: default value for query_comment is None 2020-03-12 16:59:30 +03:00
Jeremy Cohen
be1ca971be Rm failing irrelevant tint tests 2020-03-12 09:53:15 -04:00
ilkin Balkanay
5fa59d86e7 handle unset query comment 2020-03-12 16:45:36 +03:00
ilkin Balkanay
be80009aca return default comment if config.query_comment is None 2020-03-12 16:34:30 +03:00
ilkin Balkanay
f18f6af5e0 fixed mypy error 2020-03-12 16:25:58 +03:00
ilkin Balkanay
65f14723ca added unit test for query_headers.py 2020-03-12 15:46:24 +03:00
ilkin Balkanay
6910847c71 pass unit tests 2020-03-12 13:35:32 +03:00
ilkin Balkanay
62b19b53f9 added query comment append feature (wip) 2020-03-12 10:28:27 +03:00
Jeremy Cohen
8fb6592c6a Rework insert_overwrite incremental strategy 2020-03-11 22:21:11 -04:00
Jacob Beck
cda35e2a9a Bump version: 0.16.0rc2 → 0.16.0rc3 2020-03-11 09:20:06 -06:00
Jacob Beck
4472719362 fix twine check 2020-03-11 09:20:02 -06:00
Jacob Beck
6c12b7a951 Update changelog for rc3 2020-03-11 09:07:35 -06:00
Jacob Beck
ad3b63ed5d Merge pull request #2187 from fishtown-analytics/fix/no-creating-quoted-schemas
Do not create already-existing schemas (#2186)
2020-03-10 22:00:38 -06:00
Jacob Beck
e7545ad183 handle null database/schema issues 2020-03-10 21:29:11 -06:00
Jacob Beck
e390595551 when creating missing schemas, list the quoted database form, but use the unquoted for comparisons
add a test
2020-03-06 15:08:33 -07:00
Jacob Beck
b77b3a4566 Merge pull request #2184 from fishtown-analytics/fix/ugly-errors-undefined-values
For undefined variables, explicitly raise a useful error on pickling
2020-03-06 13:26:41 -07:00
Jacob Beck
16692e2e5f tests 2020-03-06 08:41:26 -07:00
Jacob Beck
757bdf69dc For undefined variables, explicitly raise a useful error on pickling 2020-03-06 08:31:39 -07:00
Jacob Beck
a259f154da Bump version: 0.16.0b3 → 0.17.0a1 2020-03-06 08:25:03 -07:00
Jacob Beck
e441c6dfa7 Merge pull request #2119 from fishtown-analytics/feature/pkg-build-script
Feature: package build script + Docker build script
2020-03-06 08:04:16 -07:00
Jacob Beck
c4c7af88c0 PR feedback + discovered small issues
run twine check before uploading
don't install netcat curl ssh on the docker image
skip the dbt resource block initial line when parsing the poet output
unlink existing versions so 'dbt install Formula/...' actually works
Wait a bit for pypi to settle down after uploading to pypi, add retries
2020-03-04 18:52:39 -07:00
Jacob Beck
d78b249827 Fix an issue in 0.16.0b3 where files were erroneously included
Previous release files that didn't exist in the new version were ending up in
the wheel (but not source dist). Attempt to fix that by removing build/
directories when we remove dist/ directories
2020-03-04 18:52:39 -07:00
Jacob Beck
af8cdedc07 Build docker images + requirements.txt files
some enhancements
2020-03-04 18:52:39 -07:00
Jacob Beck
fc7a62b650 add an initial pass at a dbt package build script
Bumps the version (including commit)
builds pypi packages
uploads them to test pypi + prompts for enter/ctrl+c
uploads them to regular pypi
builds homebrew package
overwrites the default homebrew package, if you want
makes appropriate commits
Has a bunch of flags

Requires python 3.8 (homebrew's python version)
Requires .pypirc w/creds
Does not push, so no gh creds required (should it?)

appease homebrew audit:

"version" is supplied by the url
dependencies must be alphabetized
whitespace/newline stuff
2020-03-04 18:52:39 -07:00
Jacob Beck
7919c90b81 Bump version: 0.16.0rc1 → 0.16.0rc2 2020-03-04 16:39:43 -07:00
Jacob Beck
1854d20566 Merge pull request #2181 from fishtown-analytics/fix/pin-another-dang-dependency
Pin ciffi to snowflake's versions
2020-03-04 16:29:57 -07:00
Jacob Beck
c5c5c3d863 cffi released a new version, breaking snowflake as a dependency due to version conflicts 2020-03-04 15:58:06 -07:00
Jacob Beck
6c47ae1d88 Bump version: 0.16.0b3 → 0.16.0rc1 2020-03-04 15:27:00 -07:00
Jacob Beck
70ad200dca Set release version/date in changelog 2020-03-04 15:26:33 -07:00
Drew Banin
341956768a Merge pull request #2153 from fishtown-analytics/feature/bq-insert-overwrite
Feature/bq incremental strategy insert_overwrite
2020-03-04 16:56:42 -05:00
Drew Banin
0656477131 Merge branch 'dev/barbara-gittings' of github.com:fishtown-analytics/dbt into feature/bq-insert-overwrite 2020-03-04 13:45:11 -05:00
Jeremy Cohen
e878d0e76e Add insert_overwrite as incremental strategy on BQ 2020-03-04 13:43:56 -05:00
Drew Banin
dc6a38c360 Merge pull request #2148 from fishtown-analytics/0.16.0-changelog-updates
Update CHANGELOG.md
2020-03-04 12:57:40 -05:00
Drew Banin
f883e11c7a Merge branch 'dev/barbara-gittings' into 0.16.0-changelog-updates 2020-03-04 12:57:02 -05:00
Drew Banin
7e4402b128 Merge pull request #2179 from fishtown-analytics/0.16.0-docs-site-2
Upgrade docs site to support 0.16.0 functionality
2020-03-04 12:54:41 -05:00
Drew Banin
e045e14522 Upgrade docs site to support 0.16.0 functionality 2020-03-04 11:59:31 -05:00
Jacob Beck
f209e17215 Merge pull request #2171 from fishtown-analytics/feature/use-show-schemas-snowflake
use show schemas for snowflake list_schemas (#2166)
2020-03-02 21:27:49 -07:00
Drew Banin
07461d24e0 Merge branch 'dev/barbara-gittings' of github.com:fishtown-analytics/dbt into 0.16.0-changelog-updates 2020-03-02 15:34:09 -05:00
Jacob Beck
47cef1d907 PR feedback: Add exception handler around macro execution, add a message about what is going on when list_schemas fails 2020-03-01 20:48:15 -07:00
Jacob Beck
c59adc3369 strip out the database qutoes on bigquery for the API 2020-02-28 12:04:31 -07:00
Jacob Beck
cc3ba20ec9 refactor, fix flake8/mypy 2020-02-28 11:26:34 -07:00
Jacob Beck
562f3d0bb7 use show schemas for snowflake list_schemas
Also fix up the changelog to better reflect releases
2020-02-28 10:30:57 -07:00
Jacob Beck
e8d321cbab Merge pull request #2164 from mhmcdonald/feature/snowflake_key
removing the requirement to have a passphrase on a Snowflake key
2020-02-27 18:36:51 -07:00
Mark McDonald
1edffac2bc Merge branch 'dev/barbara-gittings' into feature/snowflake_key 2020-02-27 16:05:29 -06:00
Jacob Beck
735ffb3c92 Merge pull request #2154 from dholleran-lendico/dev/barbara-gittings
Feature: adding optional sslmode parameter to postgres connection
2020-02-27 13:12:25 -07:00
Mark McDonald
66b60d860e adding myself to contributors 2020-02-27 11:40:09 -06:00
Mark McDonald
f99d31f7c5 Update CHANGELOG.md
Co-Authored-By: Jacob Beck <beckjake@users.noreply.github.com>
2020-02-27 11:34:06 -06:00
dholleran-lendico
170b1d80b5 Update plugins/postgres/dbt/adapters/postgres/connections.py
Co-Authored-By: Jacob Beck <beckjake@users.noreply.github.com>
2020-02-27 18:24:03 +01:00
dholleran-lendico
10846653f0 Merge branch 'dev/barbara-gittings' into dev/barbara-gittings 2020-02-27 18:21:31 +01:00
Jacob Beck
5d4d4f32d2 Merge pull request #2157 from fishtown-analytics/feature/threadpool-startup
Use threadpools for filling the cache and listing schemas (#2127)
2020-02-27 09:52:00 -07:00
Mark McDonald
1f49150d30 updating changelog 2020-02-27 09:37:36 -06:00
Mark McDonald
3f583bd927 removing the requirement to have a passphrase on a Snowflake key 2020-02-27 09:31:01 -06:00
Jacob Beck
da46a679f3 flake8 2020-02-25 15:52:12 -07:00
Jacob Beck
ed38cbbc0b Use threadpools for filling the cache and listing schemas 2020-02-25 11:43:07 -07:00
Daniel Holleran
ad91636c74 updating CHANGELOG 2020-02-25 12:10:15 +01:00
Daniel Holleran
5543dab97c uncommenting sslmode in postgres _coonection_keys 2020-02-25 11:52:37 +01:00
Daniel Holleran
d4ae9a89df adding 'sslmode' parameter to postgres connection 2020-02-25 11:49:06 +01:00
Drew Banin
5b7c81eb2d Update CHANGELOG.md 2020-02-19 14:53:53 -05:00
Drew Banin
1dd4187cd0 Merge branch '0.14.latest' 2019-09-05 14:32:23 -04:00
Connor McArthur
9e36ebdaab Merge branch '0.13.latest' of github.com:fishtown-analytics/dbt 2019-03-21 13:27:24 -04:00
Drew Banin
aaa0127354 Merge pull request #1241 from fishtown-analytics/0.12.latest
Merge 0.12.latest into master
2019-01-15 17:01:16 -05:00
Drew Banin
e60280c4d6 Merge branch '0.12.latest' 2018-11-15 12:24:05 -05:00
Drew Banin
aef7866e29 Update CHANGELOG.md 2018-11-13 10:36:35 -05:00
Drew Banin
70694e3bb9 Merge pull request #1118 from fishtown-analytics/0.12.latest
merge 0.12.latest to master
2018-11-13 10:19:56 -05:00
4938 changed files with 104891 additions and 17283 deletions

View File

@@ -1,23 +1,27 @@
[bumpversion]
current_version = 0.16.0b3
current_version = 0.21.0b1
parse = (?P<major>\d+)
\.(?P<minor>\d+)
\.(?P<patch>\d+)
((?P<prerelease>[a-z]+)(?P<num>\d+))?
((?P<prekind>a|b|rc)
(?P<pre>\d+) # pre-release version num
)?
serialize =
{major}.{minor}.{patch}{prerelease}{num}
{major}.{minor}.{patch}{prekind}{pre}
{major}.{minor}.{patch}
commit = False
tag = False
[bumpversion:part:prerelease]
[bumpversion:part:prekind]
first_value = a
optional_value = final
values =
a
b
rc
final
[bumpversion:part:num]
[bumpversion:part:pre]
first_value = 1
[bumpversion:file:setup.py]
@@ -26,6 +30,8 @@ first_value = 1
[bumpversion:file:core/dbt/version.py]
[bumpversion:file:core/scripts/create_adapter_plugins.py]
[bumpversion:file:plugins/postgres/setup.py]
[bumpversion:file:plugins/redshift/setup.py]
@@ -34,3 +40,11 @@ first_value = 1
[bumpversion:file:plugins/bigquery/setup.py]
[bumpversion:file:plugins/postgres/dbt/adapters/postgres/__version__.py]
[bumpversion:file:plugins/redshift/dbt/adapters/redshift/__version__.py]
[bumpversion:file:plugins/snowflake/dbt/adapters/snowflake/__version__.py]
[bumpversion:file:plugins/bigquery/dbt/adapters/bigquery/__version__.py]

View File

@@ -1,171 +0,0 @@
version: 2
jobs:
unit:
docker: &test_only
- image: fishtownjacob/test-container:4
environment:
DBT_INVOCATION_ENV: circle
steps:
- checkout
- run: tox -e flake8,mypy,unit-py36,unit-py38
build-wheels:
docker: *test_only
steps:
- checkout
- run:
name: Build wheels
command: |
python3.8 -m venv "${PYTHON_ENV}"
export PYTHON_BIN="${PYTHON_ENV}/bin/python"
$PYTHON_BIN -m pip install -U pip setuptools
$PYTHON_BIN -m pip install -r requirements.txt
$PYTHON_BIN -m pip install -r dev_requirements.txt
/bin/bash ./scripts/build-wheels.sh
environment:
PYTHON_ENV: /home/tox/build_venv/
- store_artifacts:
path: ./dist
destination: dist
integration-postgres-py36:
docker: &test_and_postgres
- image: fishtownjacob/test-container:4
environment:
DBT_INVOCATION_ENV: circle
- image: postgres
name: database
environment: &pgenv
POSTGRES_USER: "root"
POSTGRES_PASSWORD: "password"
POSTGRES_DB: "dbt"
steps:
- checkout
- run: &setupdb
name: Setup postgres
command: bash test/setup_db.sh
environment:
PGHOST: database
PGUSER: root
PGPASSWORD: password
PGDATABASE: postgres
- run:
name: Run tests
command: tox -e integration-postgres-py36
- store_artifacts:
path: ./logs
integration-snowflake-py36:
docker: *test_only
steps:
- checkout
- run:
name: Run tests
command: tox -e integration-snowflake-py36
no_output_timeout: 1h
- store_artifacts:
path: ./logs
integration-redshift-py36:
docker: *test_only
steps:
- checkout
- run:
name: Run tests
command: tox -e integration-redshift-py36
- store_artifacts:
path: ./logs
integration-bigquery-py36:
docker: *test_only
steps:
- checkout
- run:
name: Run tests
command: tox -e integration-bigquery-py36
- store_artifacts:
path: ./logs
integration-postgres-py38:
docker: *test_and_postgres
steps:
- checkout
- run: *setupdb
- run:
name: Run tests
command: tox -e integration-postgres-py38
- store_artifacts:
path: ./logs
integration-snowflake-py38:
docker: *test_only
steps:
- checkout
- run:
name: Run tests
command: tox -e integration-snowflake-py38
no_output_timeout: 1h
- store_artifacts:
path: ./logs
integration-redshift-py38:
docker: *test_only
steps:
- checkout
- run:
name: Run tests
command: tox -e integration-redshift-py38
- store_artifacts:
path: ./logs
integration-bigquery-py38:
docker: *test_only
steps:
- checkout
- run:
name: Run tests
command: tox -e integration-bigquery-py38
- store_artifacts:
path: ./logs
workflows:
version: 2
test-everything:
jobs:
- unit
- integration-postgres-py36:
requires:
- unit
- integration-redshift-py36:
requires:
- integration-postgres-py36
filters: &no_forks
branches:
# Forked pull requests have CIRCLE_BRANCH set to pull/XXX
ignore: /pull\/[0-9]+/
- integration-bigquery-py36:
requires:
- integration-postgres-py36
filters: *no_forks
- integration-snowflake-py36:
requires:
- integration-postgres-py36
filters: *no_forks
- integration-postgres-py38:
requires:
- unit
- integration-redshift-py38:
requires:
- integration-postgres-py38
filters: *no_forks
- integration-bigquery-py38:
requires:
- integration-postgres-py38
filters: *no_forks
- integration-snowflake-py38:
requires:
- integration-postgres-py38
filters: *no_forks
- build-wheels:
requires:
- unit
- integration-postgres-py36
- integration-redshift-py36
- integration-bigquery-py36
- integration-snowflake-py36
- integration-postgres-py38
- integration-redshift-py38
- integration-bigquery-py38
- integration-snowflake-py38
filters: *no_forks

View File

@@ -0,0 +1,27 @@
---
name: Beta minor version release
about: Creates a tracking checklist of items for a Beta minor version release
title: "[Tracking] v#.##.#B# release "
labels: 'release'
assignees: ''
---
### Release Core
- [ ] [Engineering] Follow [dbt-release workflow](https://www.notion.so/dbtlabs/Releasing-b97c5ea9a02949e79e81db3566bbc8ef#03ff37da697d4d8ba63d24fae1bfa817)
- [ ] [Engineering] Verify new release branch is created in the repo
- [ ] [Product] Finalize migration guide (next.docs.getdbt.com)
### Release Cloud
- [ ] [Engineering] Create a platform issue to update dbt Cloud and verify it is completed. [Example issue](https://github.com/dbt-labs/dbt-cloud/issues/3481)
- [ ] [Engineering] Determine if schemas have changed. If so, generate new schemas and push to schemas.getdbt.com
### Announce
- [ ] [Product] Announce in dbt Slack
### Post-release
- [ ] [Engineering] [Bump plugin versions](https://www.notion.so/dbtlabs/Releasing-b97c5ea9a02949e79e81db3566bbc8ef#f01854e8da3641179fbcbe505bdf515c) (dbt-spark + dbt-presto), add compatibility as needed
- [ ] [Spark](https://github.com/dbt-labs/dbt-spark)
- [ ] [Presto](https://github.com/dbt-labs/dbt-presto)
- [ ] [Engineering] Create a platform issue to update dbt-spark versions to dbt Cloud. [Example issue](https://github.com/dbt-labs/dbt-cloud/issues/3481)
- [ ] [Engineering] Create an epic for the RC release

View File

@@ -18,3 +18,6 @@ Is this feature database-specific? Which database(s) is/are relevant? Please inc
### Who will this benefit?
What kind of use case will this feature be useful for? Please be specific and provide examples, this will help us prioritize properly.
### Are you interested in contributing this feature?
Let us know if you want to write some code, and how we can help.

View File

@@ -0,0 +1,28 @@
---
name: Final minor version release
about: Creates a tracking checklist of items for a final minor version release
title: "[Tracking] v#.##.# final release "
labels: 'release'
assignees: ''
---
### Release Core
- [ ] [Engineering] Verify all necessary changes exist on the release branch
- [ ] [Engineering] Follow [dbt-release workflow](https://www.notion.so/dbtlabs/Releasing-b97c5ea9a02949e79e81db3566bbc8ef#03ff37da697d4d8ba63d24fae1bfa817)
- [ ] [Product] Merge `next` into `current` for docs.getdbt.com
### Release Cloud
- [ ] [Engineering] Create a platform issue to update dbt Cloud and verify it is completed. [Example issue](https://github.com/dbt-labs/dbt-cloud/issues/3481)
- [ ] [Engineering] Determine if schemas have changed. If so, generate new schemas and push to schemas.getdbt.com
### Announce
- [ ] [Product] Update discourse
- [ ] [Product] Announce in dbt Slack
### Post-release
- [ ] [Engineering] [Bump plugin versions](https://www.notion.so/dbtlabs/Releasing-b97c5ea9a02949e79e81db3566bbc8ef#f01854e8da3641179fbcbe505bdf515c) (dbt-spark + dbt-presto), add compatibility as needed
- [ ] [Spark](https://github.com/dbt-labs/dbt-spark)
- [ ] [Presto](https://github.com/dbt-labs/dbt-presto)
- [ ] [Engineering] Create a platform issue to update dbt-spark versions to dbt Cloud. [Example issue](https://github.com/dbt-labs/dbt-cloud/issues/3481)
- [ ] [Product] Release new version of dbt-utils with new dbt version compatibility. If there are breaking changes requiring a minor version, plan upgrades of other packages that depend on dbt-utils.

View File

@@ -0,0 +1,29 @@
---
name: RC minor version release
about: Creates a tracking checklist of items for a RC minor version release
title: "[Tracking] v#.##.#RC# release "
labels: 'release'
assignees: ''
---
### Release Core
- [ ] [Engineering] Verify all necessary changes exist on the release branch
- [ ] [Engineering] Follow [dbt-release workflow](https://www.notion.so/dbtlabs/Releasing-b97c5ea9a02949e79e81db3566bbc8ef#03ff37da697d4d8ba63d24fae1bfa817)
- [ ] [Product] Update migration guide (next.docs.getdbt.com)
### Release Cloud
- [ ] [Engineering] Create a platform issue to update dbt Cloud and verify it is completed. [Example issue](https://github.com/dbt-labs/dbt-cloud/issues/3481)
- [ ] [Engineering] Determine if schemas have changed. If so, generate new schemas and push to schemas.getdbt.com
### Announce
- [ ] [Product] Publish discourse
- [ ] [Product] Announce in dbt Slack
### Post-release
- [ ] [Engineering] [Bump plugin versions](https://www.notion.so/dbtlabs/Releasing-b97c5ea9a02949e79e81db3566bbc8ef#f01854e8da3641179fbcbe505bdf515c) (dbt-spark + dbt-presto), add compatibility as needed
- [ ] [Spark](https://github.com/dbt-labs/dbt-spark)
- [ ] [Presto](https://github.com/dbt-labs/dbt-presto)
- [ ] [Engineering] Create a platform issue to update dbt-spark versions to dbt Cloud. [Example issue](https://github.com/dbt-labs/dbt-cloud/issues/3481)
- [ ] [Product] Release new version of dbt-utils with new dbt version compatibility. If there are breaking changes requiring a minor version, plan upgrades of other packages that depend on dbt-utils.
- [ ] [Engineering] Create an epic for the final release

View File

@@ -0,0 +1,10 @@
name: "Set up postgres (linux)"
description: "Set up postgres service on linux vm for dbt integration tests"
runs:
using: "composite"
steps:
- shell: bash
run: |
sudo systemctl start postgresql.service
pg_isready
sudo -u postgres bash ${{ github.action_path }}/setup_db.sh

View File

@@ -0,0 +1 @@
../../../test/setup_db.sh

View File

@@ -0,0 +1,24 @@
name: "Set up postgres (macos)"
description: "Set up postgres service on macos vm for dbt integration tests"
runs:
using: "composite"
steps:
- shell: bash
run: |
brew services start postgresql
echo "Check PostgreSQL service is running"
i=10
COMMAND='pg_isready'
while [ $i -gt -1 ]; do
if [ $i == 0 ]; then
echo "PostgreSQL service not ready, all attempts exhausted"
exit 1
fi
echo "Check PostgreSQL service status"
eval $COMMAND && break
echo "PostgreSQL service not ready, wait 10 more sec, attempts left: $i"
sleep 10
((i--))
done
createuser -s postgres
bash ${{ github.action_path }}/setup_db.sh

View File

@@ -0,0 +1 @@
../../../test/setup_db.sh

View File

@@ -0,0 +1,12 @@
name: "Set up postgres (windows)"
description: "Set up postgres service on windows vm for dbt integration tests"
runs:
using: "composite"
steps:
- shell: pwsh
run: |
$pgService = Get-Service -Name postgresql*
Set-Service -InputObject $pgService -Status running -StartupType automatic
Start-Process -FilePath "$env:PGBIN\pg_isready" -Wait -PassThru
$env:Path += ";$env:PGBIN"
bash ${{ github.action_path }}/setup_db.sh

View File

@@ -0,0 +1 @@
../../../test/setup_db.sh

45
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
version: 2
updates:
# python dependencies
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
rebase-strategy: "disabled"
- package-ecosystem: "pip"
directory: "/core"
schedule:
interval: "daily"
rebase-strategy: "disabled"
- package-ecosystem: "pip"
directory: "/plugins/bigquery"
schedule:
interval: "daily"
rebase-strategy: "disabled"
- package-ecosystem: "pip"
directory: "/plugins/postgres"
schedule:
interval: "daily"
rebase-strategy: "disabled"
- package-ecosystem: "pip"
directory: "/plugins/redshift"
schedule:
interval: "daily"
rebase-strategy: "disabled"
- package-ecosystem: "pip"
directory: "/plugins/snowflake"
schedule:
interval: "daily"
rebase-strategy: "disabled"
# docker dependencies
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
rebase-strategy: "disabled"
- package-ecosystem: "docker"
directory: "/docker"
schedule:
interval: "weekly"
rebase-strategy: "disabled"

View File

@@ -9,14 +9,13 @@ resolves #
resolves #1234
-->
### Description
<!--- Describe the Pull Request here -->
### Checklist
- [ ] 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
- [ ] This PR includes tests, or tests are not required/relevant for this PR
- [ ] I have updated the `CHANGELOG.md` and added information about my change to the "dbt next" section.
- [ ] 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
- [ ] This PR includes tests, or tests are not required/relevant for this PR
- [ ] I have updated the `CHANGELOG.md` and added information about my change to the "dbt next" section.

View File

@@ -0,0 +1,95 @@
module.exports = ({ context }) => {
const defaultPythonVersion = "3.8";
const supportedPythonVersions = ["3.6", "3.7", "3.8", "3.9"];
const supportedAdapters = ["snowflake", "postgres", "bigquery", "redshift"];
// if PR, generate matrix based on files changed and PR labels
if (context.eventName.includes("pull_request")) {
// `changes` is a list of adapter names that have related
// file changes in the PR
// ex: ['postgres', 'snowflake']
const changes = JSON.parse(process.env.CHANGES);
const labels = context.payload.pull_request.labels.map(({ name }) => name);
console.log("labels", labels);
console.log("changes", changes);
const testAllLabel = labels.includes("test all");
const include = [];
for (const adapter of supportedAdapters) {
if (
changes.includes(adapter) ||
testAllLabel ||
labels.includes(`test ${adapter}`)
) {
for (const pythonVersion of supportedPythonVersions) {
if (
pythonVersion === defaultPythonVersion ||
labels.includes(`test python${pythonVersion}`) ||
testAllLabel
) {
// always run tests on ubuntu by default
include.push({
os: "ubuntu-latest",
adapter,
"python-version": pythonVersion,
});
if (labels.includes("test windows") || testAllLabel) {
include.push({
os: "windows-latest",
adapter,
"python-version": pythonVersion,
});
}
if (labels.includes("test macos") || testAllLabel) {
include.push({
os: "macos-latest",
adapter,
"python-version": pythonVersion,
});
}
}
}
}
}
console.log("matrix", { include });
return {
include,
};
}
// if not PR, generate matrix of python version, adapter, and operating
// system to run integration tests on
const include = [];
// run for all adapters and python versions on ubuntu
for (const adapter of supportedAdapters) {
for (const pythonVersion of supportedPythonVersions) {
include.push({
os: 'ubuntu-latest',
adapter: adapter,
"python-version": pythonVersion,
});
}
}
// additionally include runs for all adapters, on macos and windows,
// but only for the default python version
for (const adapter of supportedAdapters) {
for (const operatingSystem of ["windows-latest", "macos-latest"]) {
include.push({
os: operatingSystem,
adapter: adapter,
"python-version": defaultPythonVersion,
});
}
}
console.log("matrix", { include });
return {
include,
};
};

266
.github/workflows/integration.yml vendored Normal file
View File

@@ -0,0 +1,266 @@
# **what?**
# This workflow runs all integration tests for supported OS
# and python versions and core adapters. If triggered by PR,
# the workflow will only run tests for adapters related
# to code changes. Use the `test all` and `test ${adapter}`
# label to run all or additional tests. Use `ok to test`
# label to mark PRs from forked repositories that are safe
# to run integration tests for. Requires secrets to run
# against different warehouses.
# **why?**
# This checks the functionality of dbt from a user's perspective
# and attempts to catch functional regressions.
# **when?**
# This workflow will run on every push to a protected branch
# and when manually triggered. It will also run for all PRs, including
# PRs from forks. The workflow will be skipped until there is a label
# to mark the PR as safe to run.
name: Adapter Integration Tests
on:
# pushes to release branches
push:
branches:
- "main"
- "develop"
- "*.latest"
- "releases/*"
# all PRs, important to note that `pull_request_target` workflows
# will run in the context of the target branch of a PR
pull_request_target:
# manual tigger
workflow_dispatch:
# explicitly turn off permissions for `GITHUB_TOKEN`
permissions: read-all
# will cancel previous workflows triggered by the same event and for the same ref for PRs or same SHA otherwise
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.ref || github.sha }}
cancel-in-progress: true
# sets default shell to bash, for all operating systems
defaults:
run:
shell: bash
jobs:
# generate test metadata about what files changed and the testing matrix to use
test-metadata:
# run if not a PR from a forked repository or has a label to mark as safe to test
if: >-
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository ||
contains(github.event.pull_request.labels.*.name, 'ok to test')
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate-matrix.outputs.result }}
steps:
- name: Check out the repository (non-PR)
if: github.event_name != 'pull_request_target'
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Check out the repository (PR)
if: github.event_name == 'pull_request_target'
uses: actions/checkout@v2
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}
- name: Check if relevant files changed
# 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: get-changes
with:
token: ${{ secrets.GITHUB_TOKEN }}
filters: |
postgres:
- 'core/**'
- 'plugins/postgres/**'
- 'dev-requirements.txt'
snowflake:
- 'core/**'
- 'plugins/snowflake/**'
bigquery:
- 'core/**'
- 'plugins/bigquery/**'
redshift:
- 'core/**'
- 'plugins/redshift/**'
- 'plugins/postgres/**'
- name: Generate integration test matrix
id: generate-matrix
uses: actions/github-script@v4
env:
CHANGES: ${{ steps.get-changes.outputs.changes }}
with:
script: |
const script = require('./.github/scripts/integration-test-matrix.js')
const matrix = script({ context })
console.log(matrix)
return matrix
test:
name: ${{ matrix.adapter }} / python ${{ matrix.python-version }} / ${{ matrix.os }}
# run if not a PR from a forked repository or has a label to mark as safe to test
# also checks that the matrix generated is not empty
if: >-
needs.test-metadata.outputs.matrix &&
fromJSON( needs.test-metadata.outputs.matrix ).include[0] &&
(
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository ||
contains(github.event.pull_request.labels.*.name, 'ok to test')
)
runs-on: ${{ matrix.os }}
needs: test-metadata
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.test-metadata.outputs.matrix) }}
env:
TOXENV: integration-${{ matrix.adapter }}
PYTEST_ADDOPTS: "-v --color=yes -n4 --csv integration_results.csv"
DBT_INVOCATION_ENV: github-actions
steps:
- name: Check out the repository
if: github.event_name != 'pull_request_target'
uses: actions/checkout@v2
with:
persist-credentials: false
# explicity checkout the branch for the PR,
# this is necessary for the `pull_request_target` event
- name: Check out the repository (PR)
if: github.event_name == 'pull_request_target'
uses: actions/checkout@v2
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Set up postgres (linux)
if: |
matrix.adapter == 'postgres' &&
runner.os == 'Linux'
uses: ./.github/actions/setup-postgres-linux
- name: Set up postgres (macos)
if: |
matrix.adapter == 'postgres' &&
runner.os == 'macOS'
uses: ./.github/actions/setup-postgres-macos
- name: Set up postgres (windows)
if: |
matrix.adapter == 'postgres' &&
runner.os == 'Windows'
uses: ./.github/actions/setup-postgres-windows
- name: Install python dependencies
run: |
pip install --upgrade pip
pip install tox
pip --version
tox --version
- name: Run tox (postgres)
if: matrix.adapter == 'postgres'
run: tox
- name: Run tox (redshift)
if: matrix.adapter == 'redshift'
env:
REDSHIFT_TEST_DBNAME: ${{ secrets.REDSHIFT_TEST_DBNAME }}
REDSHIFT_TEST_PASS: ${{ secrets.REDSHIFT_TEST_PASS }}
REDSHIFT_TEST_USER: ${{ secrets.REDSHIFT_TEST_USER }}
REDSHIFT_TEST_PORT: ${{ secrets.REDSHIFT_TEST_PORT }}
REDSHIFT_TEST_HOST: ${{ secrets.REDSHIFT_TEST_HOST }}
run: tox
- name: Run tox (snowflake)
if: matrix.adapter == 'snowflake'
env:
SNOWFLAKE_TEST_ACCOUNT: ${{ secrets.SNOWFLAKE_TEST_ACCOUNT }}
SNOWFLAKE_TEST_PASSWORD: ${{ secrets.SNOWFLAKE_TEST_PASSWORD }}
SNOWFLAKE_TEST_USER: ${{ secrets.SNOWFLAKE_TEST_USER }}
SNOWFLAKE_TEST_WAREHOUSE: ${{ secrets.SNOWFLAKE_TEST_WAREHOUSE }}
SNOWFLAKE_TEST_OAUTH_REFRESH_TOKEN: ${{ secrets.SNOWFLAKE_TEST_OAUTH_REFRESH_TOKEN }}
SNOWFLAKE_TEST_OAUTH_CLIENT_ID: ${{ secrets.SNOWFLAKE_TEST_OAUTH_CLIENT_ID }}
SNOWFLAKE_TEST_OAUTH_CLIENT_SECRET: ${{ secrets.SNOWFLAKE_TEST_OAUTH_CLIENT_SECRET }}
SNOWFLAKE_TEST_ALT_DATABASE: ${{ secrets.SNOWFLAKE_TEST_ALT_DATABASE }}
SNOWFLAKE_TEST_ALT_WAREHOUSE: ${{ secrets.SNOWFLAKE_TEST_ALT_WAREHOUSE }}
SNOWFLAKE_TEST_DATABASE: ${{ secrets.SNOWFLAKE_TEST_DATABASE }}
SNOWFLAKE_TEST_QUOTED_DATABASE: ${{ secrets.SNOWFLAKE_TEST_QUOTED_DATABASE }}
SNOWFLAKE_TEST_ROLE: ${{ secrets.SNOWFLAKE_TEST_ROLE }}
run: tox
- name: Run tox (bigquery)
if: matrix.adapter == 'bigquery'
env:
BIGQUERY_TEST_SERVICE_ACCOUNT_JSON: ${{ secrets.BIGQUERY_TEST_SERVICE_ACCOUNT_JSON }}
BIGQUERY_TEST_ALT_DATABASE: ${{ secrets.BIGQUERY_TEST_ALT_DATABASE }}
run: tox
- uses: actions/upload-artifact@v2
if: always()
with:
name: logs
path: ./logs
- name: Get current date
if: always()
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" #no colons allowed for artifacts
- uses: actions/upload-artifact@v2
if: always()
with:
name: integration_results_${{ matrix.python-version }}_${{ matrix.os }}_${{ matrix.adapter }}-${{ steps.date.outputs.date }}.csv
path: integration_results.csv
require-label-comment:
runs-on: ubuntu-latest
needs: test
permissions:
pull-requests: write
steps:
- name: Needs permission PR comment
if: >-
needs.test.result == 'skipped' &&
github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository
uses: unsplash/comment-on-pr@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
msg: |
"You do not have permissions to run integration tests, @dbt-labs/core "\
"needs to label this PR with `ok to test` in order to run integration tests!"
check_for_duplicate_msg: true

206
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,206 @@
# **what?**
# Runs code quality checks, unit tests, and verifies python build on
# all code commited to the repository. 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 for dbt meets a certain quality standard.
# **when?**
# This will run for all PRs, when code is pushed to a release
# branch, and when manually triggered.
name: Tests and Code Checks
on:
push:
branches:
- "main"
- "develop"
- "*.latest"
- "releases/*"
pull_request:
workflow_dispatch:
permissions: read-all
# will cancel previous workflows triggered by the same event and for the same ref for PRs or same SHA otherwise
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.ref || github.sha }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
code-quality:
name: ${{ matrix.toxenv }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
toxenv: [flake8, mypy]
env:
TOXENV: ${{ matrix.toxenv }}
PYTEST_ADDOPTS: "-v --color=yes"
steps:
- name: Check out the repository
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v2
- name: Install python dependencies
run: |
pip install --upgrade pip
pip install tox
pip --version
tox --version
- name: Run tox
run: tox
unit:
name: unit test / python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8] # TODO: support unit testing for python 3.9 (https://github.com/dbt-labs/dbt/issues/3689)
env:
TOXENV: "unit"
PYTEST_ADDOPTS: "-v --color=yes --csv unit_results.csv"
steps:
- name: Check out the repository
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install python dependencies
run: |
pip install --upgrade pip
pip install tox
pip --version
tox --version
- name: Run tox
run: tox
- name: Get current date
if: always()
id: date
run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" #no colons allowed for artifacts
- uses: actions/upload-artifact@v2
if: always()
with:
name: unit_results_${{ matrix.python-version }}-${{ steps.date.outputs.date }}.csv
path: unit_results.csv
build:
name: build packages
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install python dependencies
run: |
pip install --upgrade pip
pip install --upgrade setuptools wheel twine check-wheel-contents
pip --version
- name: Build distributions
run: ./scripts/build-dist.sh
- name: Show distributions
run: ls -lh dist/
- name: Check distribution descriptions
run: |
twine check dist/*
- name: Check wheel contents
run: |
check-wheel-contents dist/*.whl --ignore W007,W008
- uses: actions/upload-artifact@v2
with:
name: dist
path: dist/
test-build:
name: verify packages / python ${{ matrix.python-version }} / ${{ matrix.os }}
needs: build
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.6, 3.7, 3.8, 3.9]
steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install python dependencies
run: |
pip install --upgrade pip
pip install --upgrade wheel
pip --version
- uses: actions/download-artifact@v2
with:
name: dist
path: dist/
- name: Show distributions
run: ls -lh dist/
- name: Install wheel distributions
run: |
find ./dist/*.whl -maxdepth 1 -type f | xargs pip install --force-reinstall --find-links=dist/
- name: Check wheel distributions
run: |
dbt --version
- name: Install source distributions
run: |
find ./dist/*.gz -maxdepth 1 -type f | xargs pip install --force-reinstall --find-links=dist/
- name: Check source distributions
run: |
dbt --version

174
.github/workflows/performance.yml vendored Normal file
View File

@@ -0,0 +1,174 @@
name: Performance Regression Tests
# Schedule triggers
on:
# runs twice a day at 10:05am and 10:05pm
schedule:
- cron: "5 10,22 * * *"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
# checks fmt of runner code
# purposefully not a dependency of any other job
# will block merging, but not prevent developing
fmt:
name: Cargo fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --manifest-path performance/runner/Cargo.toml --all -- --check
# runs any tests associated with the runner
# these tests make sure the runner logic is correct
test-runner:
name: Test Runner
runs-on: ubuntu-latest
env:
# turns errors into warnings
RUSTFLAGS: "-D warnings"
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path performance/runner/Cargo.toml
# build an optimized binary to be used as the runner in later steps
build-runner:
needs: [test-runner]
name: Build Runner
runs-on: ubuntu-latest
env:
RUSTFLAGS: "-D warnings"
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: build
args: --release --manifest-path performance/runner/Cargo.toml
- uses: actions/upload-artifact@v2
with:
name: runner
path: performance/runner/target/release/runner
# run the performance measurements on the current or default branch
measure-dev:
needs: [build-runner]
name: Measure Dev Branch
runs-on: ubuntu-latest
steps:
- name: checkout dev
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2.2.2
with:
python-version: "3.8"
- name: install dbt
run: pip install -r dev-requirements.txt -r editable-requirements.txt
- name: install hyperfine
run: wget https://github.com/sharkdp/hyperfine/releases/download/v1.11.0/hyperfine_1.11.0_amd64.deb && sudo dpkg -i hyperfine_1.11.0_amd64.deb
- uses: actions/download-artifact@v2
with:
name: runner
- name: change permissions
run: chmod +x ./runner
- name: run
run: ./runner measure -b dev -p ${{ github.workspace }}/performance/projects/
- uses: actions/upload-artifact@v2
with:
name: dev-results
path: performance/results/
# run the performance measurements on the release branch which we use
# as a performance baseline. This part takes by far the longest, so
# we do everything we can first so the job fails fast.
# -----
# we need to checkout dbt twice in this job: once for the baseline dbt
# version, and once to get the latest regression testing projects,
# metrics, and runner code from the develop or current branch so that
# the calculations match for both versions of dbt we are comparing.
measure-baseline:
needs: [build-runner]
name: Measure Baseline Branch
runs-on: ubuntu-latest
steps:
- name: checkout latest
uses: actions/checkout@v2
with:
ref: "0.20.latest"
- name: Setup Python
uses: actions/setup-python@v2.2.2
with:
python-version: "3.8"
- name: move repo up a level
run: mkdir ${{ github.workspace }}/../baseline/ && cp -r ${{ github.workspace }} ${{ github.workspace }}/../baseline
- name: "[debug] ls new dbt location"
run: ls ${{ github.workspace }}/../baseline/dbt/
# installation creates egg-links so we have to preserve source
- name: install dbt from new location
run: cd ${{ github.workspace }}/../baseline/dbt/ && pip install -r dev-requirements.txt -r editable-requirements.txt
# checkout the current branch to get all the target projects
# this deletes the old checked out code which is why we had to copy before
- name: checkout dev
uses: actions/checkout@v2
- name: install hyperfine
run: wget https://github.com/sharkdp/hyperfine/releases/download/v1.11.0/hyperfine_1.11.0_amd64.deb && sudo dpkg -i hyperfine_1.11.0_amd64.deb
- uses: actions/download-artifact@v2
with:
name: runner
- name: change permissions
run: chmod +x ./runner
- name: run runner
run: ./runner measure -b baseline -p ${{ github.workspace }}/performance/projects/
- uses: actions/upload-artifact@v2
with:
name: baseline-results
path: performance/results/
# detect regressions on the output generated from measuring
# the two branches. Exits with non-zero code if a regression is detected.
calculate-regressions:
needs: [measure-dev, measure-baseline]
name: Compare Results
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: dev-results
- uses: actions/download-artifact@v2
with:
name: baseline-results
- name: "[debug] ls result files"
run: ls
- uses: actions/download-artifact@v2
with:
name: runner
- name: change permissions
run: chmod +x ./runner
- name: run calculation
run: ./runner calculate -r ./
# always attempt to upload the results even if there were regressions found
- uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
name: final-calculations
path: ./final_calculations.json

13
.gitignore vendored
View File

@@ -8,7 +8,8 @@ __pycache__/
# Distribution / packaging
.Python
env/
env*/
dbt_env/
build/
develop-eggs/
dist/
@@ -42,6 +43,7 @@ htmlcov/
.coverage
.coverage.*
.cache
.env
nosetests.xml
coverage.xml
*,cover
@@ -83,3 +85,12 @@ target/
# pycharm
.idea/
venv/
# AWS credentials
.aws/
.DS_Store
# vscode
.vscode/

49
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,49 @@
The core function of dbt is SQL compilation and execution. Users create projects of dbt resources (models, tests, seeds, snapshots, ...), defined in SQL and YAML files, and they invoke dbt to create, update, or query associated views and tables. Today, dbt makes heavy use of Jinja2 to enable the templating of SQL, and to construct a DAG (Directed Acyclic Graph) from all of the resources in a project. Users can also extend their projects by installing resources (including Jinja macros) from other projects, called "packages."
## dbt-core
Most of the python code in the repository is within the `core/dbt` directory. Currently the main subdirectories are:
- [`adapters`](core/dbt/adapters): Define base classes for behavior that is likely to differ across databases
- [`clients`](core/dbt/clients): Interface with dependencies (agate, jinja) or across operating systems
- [`config`](core/dbt/config): Reconcile user-supplied configuration from connection profiles, project files, and Jinja macros
- [`context`](core/dbt/context): Build and expose dbt-specific Jinja functionality
- [`contracts`](core/dbt/contracts): Define Python objects (dataclasses) that dbt expects to create and validate
- [`deps`](core/dbt/deps): Package installation and dependency resolution
- [`graph`](core/dbt/graph): Produce a `networkx` DAG of project resources, and selecting those resources given user-supplied criteria
- [`include`](core/dbt/include): The dbt "global project," which defines default implementations of Jinja2 macros
- [`parser`](core/dbt/parser): Read project files, validate, construct python objects
- [`rpc`](core/dbt/rpc): Provide remote procedure call server for invoking dbt, following JSON-RPC 2.0 spec
- [`task`](core/dbt/task): Set forth the actions that dbt can perform when invoked
### Invoking dbt
There are two supported ways of invoking dbt: from the command line and using an RPC server.
The "tasks" map to top-level dbt commands. So `dbt run` => task.run.RunTask, etc. Some are more like abstract base classes (GraphRunnableTask, for example) but all the concrete types outside of task/rpc should map to tasks. Currently one executes at a time. The tasks kick off their “Runners” and those do execute in parallel. The parallelism is managed via a thread pool, in GraphRunnableTask.
core/dbt/include/index.html
This is the docs website code. It comes from the dbt-docs repository, and is generated when a release is packaged.
## Adapters
dbt uses an adapter-plugin pattern to extend support to different databases, warehouses, query engines, etc. The four core adapters that are in the main repository, contained within the [`plugins`](plugins) subdirectory, are: Postgres Redshift, Snowflake and BigQuery. Other warehouses use adapter plugins defined in separate repositories (e.g. [dbt-spark](https://github.com/dbt-labs/dbt-spark), [dbt-presto](https://github.com/dbt-labs/dbt-presto)).
Each adapter is a mix of python, Jinja2, and SQL. The adapter code also makes heavy use of Jinja2 to wrap modular chunks of SQL functionality, define default implementations, and allow plugins to override it.
Each adapter plugin is a standalone python package that includes:
- `dbt/include/[name]`: A "sub-global" dbt project, of YAML and SQL files, that reimplements Jinja macros to use the adapter's supported SQL syntax
- `dbt/adapters/[name]`: Python modules that inherit, and optionally reimplement, the base adapter classes defined in dbt-core
- `setup.py`
The Postgres adapter code is the most central, and many of its implementations are used as the default defined in the dbt-core global project. The greater the distance of a data technology from Postgres, the more its adapter plugin may need to reimplement.
## Testing dbt
The [`test/`](test/) subdirectory includes unit and integration tests that run as continuous integration checks against open pull requests. Unit tests check mock inputs and outputs of specific python functions. Integration tests perform end-to-end dbt invocations against real adapters (Postgres, Redshift, Snowflake, BigQuery) and assert that the results match expectations. See [the contributing guide](CONTRIBUTING.md) for a step-by-step walkthrough of setting up a local development and testing environment.
## Everything else
- [docker](docker/): All dbt versions are published as Docker images on DockerHub. This subfolder contains the `Dockerfile` (constant) and `requirements.txt` (one for each version).
- [etc](etc/): Images for README
- [scripts](scripts/): Helper scripts for testing, releasing, and producing JSON schemas. These are not included in distributions of dbt, not are they rigorously tested—they're just handy tools for the dbt maintainers :)

File diff suppressed because it is too large Load Diff

View File

@@ -1,111 +1,151 @@
# Getting started with dbt
# Contributing to `dbt`
1. [About this document](#about-this-document)
2. [Proposing a change](#proposing-a-change)
3. [Getting the code](#getting-the-code)
4. [Setting up an environment](#setting-up-an-environment)
5. [Running `dbt` in development](#running-dbt-in-development)
6. [Testing](#testing)
7. [Submitting a Pull Request](#submitting-a-pull-request)
## About this document
This document is a guide intended for folks interested in contributing to dbt. It is not intended as a guide for end users of dbt (though if it is helpful, that's great!) and it assumes a certain level of familiarity with Python concepts such as virtualenvs, `pip`, python modules, filesystems, and so on. This guide also documents the process by which community-contributed Pull Requests can be incorporated into this repository. This guide assumes you are using macOS or Linux and are comfortable with the command line. If you get stuck while reading this guide, drop us a line in the #development channel on [slack](slack.getdbt.com).
This document is a guide intended for folks interested in contributing to `dbt`. 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`, 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.
## Contributing a change
dbt is Apache 2.0-licensed open source software. dbt is the software that 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, you are welcomed and encouraged 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, the first step is to open an issue. Please check the list of [open issues](https://github.com/fishtown-analytics/dbt/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 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 creating an issue, a dbt maintainer will follow up with you 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 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 the issue is appropriately well-scoped and describes a beneficial change to the dbt 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).
In some cases, the right resolution to an open issue might be tangential to the dbt 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 maintainers are unwilling or unable to incorporate into the dbt codebase. When it is determined that an open issue describes functionality that will not translate to a code change in the dbt repository, the issue will be tagged with the `wontfix` label (see below) and closed.
### Using issue labels
The dbt maintainers use labels to categorize open issues. Some labels indicate the databases impacted by the issue, while others describe the domain in the dbt codebase germane to the discussion. While most of these labels are self-explanatory (eg. `snowflake` or `bigquery`), there are others that are worth describing.
| tag | description |
| --- | ----------- |
| bug | This issue represents a defect or regression in dbt |
| enhancement | This issue represents net-new functionality in dbt |
| good first issue | This issue does not require deep knowledge of the dbt codebase to implement. This issue is appropriate for a first-time contributor to implement. |
| snoozed | This issue describes a good idea, but one which will probably not be addressed in a six-month time horizon. The dbt maintainers will revist these issues periodically and re-prioritize them accordingly. |
| triage | This is a new issue which has not yet been reviewed by a dbt maintainer. This label is removed when a maintainer reviews and responds to the issue. |
| stale | This is an old issue which has not recently been updated. Stale issues will periodically be closed by dbt maintainers, but they can be re-opened if the discussion is restarted. |
| wontfix | This issue does not require a code change in the dbt repository, or the maintainers are unwilling/unable to merge a Pull Request which implements the behavior described in the issue. |
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).
### Signing the CLA
All contributors to dbt must sign the [Contributor License Agreement](https://docs.getdbt.com/docs/contributor-license-agreements) to have their Pull Request merged into the dbt codebase. If you are unable to sign the CLA, then the dbt maintainers will unfortunately be unable to merge your Pull Request.
Please note that all contributors to `dbt` must sign the [Contributor License Agreement](https://docs.getdbt.com/docs/contributor-license-agreements) to have their Pull Request merged into the `dbt` codebase. If you are unable to sign the CLA, then the `dbt` maintainers will unfortunately be unable to merge your Pull Request. You are, however, welcome to open issues and comment on existing ones.
## Proposing a change
`dbt` is Apache 2.0-licensed open source software. `dbt` 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`, the first step is to open an issue. Please check the list of [open issues](https://github.com/dbt-labs/dbt/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` 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` 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` 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` 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` 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/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` 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`'s [unit](test/unit) and [integration](test/integration) tests is the tests themselves. One of the maintainers can help by pointing out relevant examples.
In some cases, the right resolution to an open issue might be tangential to the `dbt` 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` maintainers are unwilling or unable to incorporate into the `dbt` codebase. When it is determined that an open issue describes functionality that will not translate to a code change in the `dbt` repository, the issue will be tagged with the `wontfix` label (see below) and closed.
### Using issue labels
The `dbt` maintainers use labels to categorize open issues. Some labels indicate the databases impacted by the issue, while others describe the domain in the `dbt` codebase germane to the discussion. While most of these labels are self-explanatory (eg. `snowflake` or `bigquery`), there are others that are worth describing.
| tag | description |
| --- | ----------- |
| [triage](https://github.com/dbt-labs/dbt/labels/triage) | This is a new issue which has not yet been reviewed by a `dbt` maintainer. This label is removed when a maintainer reviews and responds to the issue. |
| [bug](https://github.com/dbt-labs/dbt/labels/bug) | This issue represents a defect or regression in `dbt` |
| [enhancement](https://github.com/dbt-labs/dbt/labels/enhancement) | This issue represents net-new functionality in `dbt` |
| [good first issue](https://github.com/dbt-labs/dbt/labels/good%20first%20issue) | This issue does not require deep knowledge of the `dbt` codebase to implement. This issue is appropriate for a first-time contributor. |
| [help wanted](https://github.com/dbt-labs/dbt/labels/help%20wanted) / [discussion](https://github.com/dbt-labs/dbt/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/issues/duplicate) | This issue is functionally identical to another open issue. The `dbt` maintainers will close this issue and encourage community members to focus conversation on the other one. |
| [snoozed](https://github.com/dbt-labs/dbt/labels/snoozed) | This issue describes a good idea, but one which will probably not be addressed in a six-month time horizon. The `dbt` maintainers will revist these issues periodically and re-prioritize them accordingly. |
| [stale](https://github.com/dbt-labs/dbt/labels/stale) | This is an old issue which has not recently been updated. Stale issues will periodically be closed by `dbt` maintainers, but they can be re-opened if the discussion is restarted. |
| [wontfix](https://github.com/dbt-labs/dbt/labels/wontfix) | This issue does not require a code change in the `dbt` repository, or the maintainers are unwilling/unable to merge a Pull Request which implements the behavior described in the issue. |
#### Branching Strategy
`dbt` has three types of branches:
- **Trunks** are where active development of the next release takes place. There is one trunk named `develop` 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`. 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`.
- **Feature Branches** track individual features and fixes. On completion they should be merged into the trunk brnach or a specific release branch.
## Getting the code
### Installing git
You will need `git` in order to download and modify the dbt source code. On macOS, the best way to download git is to just install Xcode.
You will need `git` in order to download and modify the `dbt` source code. On macOS, the best way to download git is to just install [Xcode](https://developer.apple.com/support/xcode/).
### External contributors
If you are not a member of the `fishtown-analytics` GitHub organization, you can contribute to dbt by forking the dbt 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` by forking the `dbt` 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 repository
2. clone your fork
1. fork the `dbt` repository
2. clone your fork locally
3. check out a new branch for your proposed changes
4. push changes to your fork
5. open a pull request against `fishtown-analytics/dbt` from your forked repository
5. open a pull request against `dbt-labs/dbt` from your forked repository
### Core contributors
If you are a member of the `fishtown-analytics` GitHub organization, you will have push access to the dbt repo. Rather than
forking dbt to make your changes, just clone the repository and push directly to a branch.
If you are a member of the `dbt-labs` GitHub organization, you will have push access to the `dbt` repo. Rather than forking `dbt` to make your changes, just clone the repository, check out a new branch, and push directly to that branch.
## Setting up an environment
To begin developing code in dbt, you should set up the following:
There are some tools that will be helpful to you in developing locally. While this is the list relevant for `dbt` development, many of these tools are used commonly across open-source python projects.
### virtualenv
### Tools
We strongly recommend using virtual environments when developing code in dbt. We recommend creating this virtualenv
in the root of the dbt repository. To create a new virtualenv, run:
```
A short list of tools used in `dbt` 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.6, Python 3.7, Python 3.8, and Python 3.9
- [`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
- [`mypy`](https://mypy.readthedocs.io/en/stable/) for static type checking
- [CircleCI](https://circleci.com/product/) and [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/)
A deep understanding of these tools in not required to effectively contribute to `dbt`, but we recommend checking out the attached documentation if you're interested in learning more about them.
#### virtual environments
We strongly recommend using virtual environments when developing code in `dbt`. We recommend creating this virtualenv
in the root of the `dbt` repository. To create a new virtualenv, run:
```sh
python3 -m venv env
source env/bin/activate
```
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. For macOS, the easiest thing to do is to [download docker for mac](https://store.docker.com/editions/community/docker-ce-desktop-mac). You'll need to make an account. On Linux, you can use one of the packages [here](https://docs.docker.com/install/#server). We recommend installing from docker.com instead of from your package manager. On Linux you also have to install docker-compose separately, follow [these instructions](https://docs.docker.com/compose/install/#install-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/).
### Installing postgres locally (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:
```
```sh
brew install postgresql
```
## Running dbt in development
## Running `dbt` in development
### Installation
First make sure that you set up your `virtualenv` as described in section _Setting up an environment_. Next, install dbt (and it's dependencies) with:
First make sure that you set up your `virtualenv` as described in [Setting up an environment](#setting-up-an-environment). Next, install `dbt` (and its dependencies) with:
```
pip install -r editable_requirements.txt
```sh
make dev
# or
pip install -r dev-requirements.txt -r editable-requirements.txt
```
When dbt is installed from source in this way, any changes you make to the dbt source code will be reflected immediately in your next `dbt` run.
When `dbt` is installed this way, any changes you make to the `dbt` source code will be reflected immediately in your next `dbt` run.
### Running dbt
### Running `dbt`
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.
@@ -113,82 +153,79 @@ Configure your [profile](https://docs.getdbt.com/docs/configure-your-profile) as
## Testing
Getting the dbt integration tests set up in your local environment will be very helpful as you start to make changes to your local version of dbt. The section that follows outlines some helpful tips for setting up the test environment.
Getting the `dbt` integration tests set up in your local environment will be very helpful as you start to make changes to your local version of `dbt`. The section that follows outlines some helpful tips for setting up the test environment.
### Tools
Since `dbt` works with a number of different databases, you will need to supply credentials for one or more of these databases in your test environment. Most organizations don't have access to each of a BigQuery, Redshift, Snowflake, and Postgres database, so it's likely that you will be unable to run every integration test locally. Fortunately, dbt Labs provides a CI environment with access to sandboxed Redshift, Snowflake, BigQuery, and Postgres databases. See the section on [_Submitting a Pull Request_](#submitting-a-pull-request) below for more information on this CI setup.
A short list of tools used in dbt testing that will be helpful to your understanding:
### Initial setup
- [virtualenv](https://virtualenv.pypa.io/en/stable/) to manage dependencies
- [tox](https://tox.readthedocs.io/en/latest/) to manage virtualenvs across python versions
- [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://gitlab.com/pycqa/flake8) for code linting
- [CircleCI](https://circleci.com/product/) and [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/)
A deep understanding of these tools in not required to effectively contribute to dbt, but we recommend checking out the attached documentation if you're interested in learning more about them.
### Running tests via Docker
dbt's unit and integration tests run in Docker. Because dbt works with a number of different databases, you will need to supply credentials for one or more of these databases in your test environment. Most organizations don't have access to each of a BigQuery, Redshift, Snowflake, and Postgres database, so it's likely that you will be unable to run every integration test locally. Fortunately, Fishtown Analytics provides a CI environment with access to sandboxed Redshift, Snowflake, BigQuery, and Postgres databases. See the section on _Submitting a Pull Request_ below for more information on this CI setup.
#### Specifying your test credentials
dbt uses test credentials specified in a `test.env` file in the root of the repository. This `test.env` file is git-ignored, but please be _extra_ careful to never check in credentials or other sensitive information when developing against dbt. To create your `test.env` file, copy the provided sample file, then supply your relevant credentials:
```
cp test.env.sample test.env
atom test.env # supply your credentials
```
We recommend starting with dbt's Postgres tests. These tests cover most of the functionality in dbt, are the fastest to run, and are the easiest to set up. dbt's test suite runs Postgres in a Docker container, so no setup should be required to run these tests. If you additionally want to test Snowflake, Bigquery, or Redshift locally you'll need to get credentials and add them to the `test.env` file.
#### Running tests
dbt's unit tests and Python linter can be run with:
```
make test-unit
```
To run the Postgres + Python 3.6 integration tests, you'll have to do one extra step of setting up the test database:
We recommend starting with `dbt`'s Postgres tests. These tests cover most of the functionality in `dbt`, 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
make setup-db
```
or, alternatively:
```sh
docker-compose up -d database
PGHOST=localhost PGUSER=root PGPASSWORD=password PGDATABASE=postgres bash test/setup_db.sh
```
To run a quick test for Python3 integration tests on Postgres, you can run:
`dbt` uses test credentials specified in a `test.env` file in the root of the repository for non-Postgres databases. This `test.env` file is git-ignored, but please be _extra_ careful to never check in credentials or other sensitive information when developing against `dbt`. To create your `test.env` file, copy the provided sample file, then supply your relevant credentials. This step is only required to use non-Postgres databases.
```
make test-quick
cp test.env.sample test.env
$EDITOR test.env
```
To run tests for a specific database, invoke `tox` directly with the required flags:
> In general, it's most important to have successful unit and Postgres tests. Once you open a PR, `dbt` will automatically run integration tests for the other three core database adapters. Of course, if you are a BigQuery user, contributing a BigQuery-only feature, it's important to run BigQuery tests as well.
### Test commands
There are a few methods for running tests locally.
#### Makefile
There are multiple targets in the Makefile to run common test suites and code
checks, most notably:
```sh
# Runs unit tests with py38 and code checks in parallel.
make test
# Runs postgres integration tests with py38 in "fail fast" mode.
make integration
```
# Run Postgres py36 tests
docker-compose run test tox -e integration-postgres-py36 -- -x
> These make targets assume you have a recent version of [`tox`](https://tox.readthedocs.io/en/latest/) installed locally,
> unless you use choose a Docker container to run tests. Run `make help` for more info.
# Run Snowflake py36 tests
docker-compose run test tox -e integration-snowflake-py36 -- -x
Check out the other targets in the Makefile to see other commonly used test
suites.
# Run BigQuery py36 tests
docker-compose run test tox -e integration-bigquery-py36 -- -x
#### `tox`
# Run Redshift py36 tests
docker-compose run test tox -e integration-redshift-py36 -- -x
[`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.6, Python 3.7, Python 3.8, `flake8` checks, and `mypy` checks in
parallel with `tox -p`. Also, you can run unit tests for specific python versions
with `tox -e py36`. The configuration for these tests in located in `tox.ini`.
#### `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:
```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
python -m pytest test/unit/test_graph.py
# run a specific unit test
python -m pytest test/unit/test_graph.py::GraphTest::test__dependency_list
```
> [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.
## Submitting a Pull Request
See the `Makefile` contents for more some other examples of ways to run `tox`.
dbt Labs provides a sandboxed Redshift, Snowflake, and BigQuery database for use in a CI environment. When pull requests are submitted to the `dbt-labs/dbt` repo, GitHub will trigger automated tests in CircleCI and Azure Pipelines.
### Submitting a Pull Request
A `dbt` 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.
Fishtown Analytics provides a sandboxed Redshift, Snowflake, and BigQuery database for use in a CI environment.
When pull requests are submitted to the `fishtown-analytics/dbt` repo, GitHub will trigger automated tests in CircleCI and Azure Pipelines. If the PR submitter is a member of the `fishtown-analytics` GitHub organization, then the credentials for these databases will be automatically supplied as environment variables in the CI test suite.
**If the PR submitter is not a member of the `fishtown-analytics` organization, then these environment variables will not be automatically supplied in the CI environment**. Once a core maintainer has taken a look at the Pull Request, they will kick off the test suite with the required credentials.
Once your tests are passing and your PR has been reviewed, a dbt 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` maintainer will merge your changes into the active development branch. And that's it! Happy developing :tada:

View File

@@ -1,33 +0,0 @@
FROM ubuntu:18.04
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && \
apt-get dist-upgrade -y && \
apt-get install -y --no-install-recommends \
netcat postgresql curl git ssh software-properties-common \
make build-essential ca-certificates libpq-dev && \
add-apt-repository ppa:deadsnakes/ppa && \
apt-get install -y \
python python-dev python-pip \
python3.6 python3.6-dev python3-pip python3.6-venv \
python3.7 python3.7-dev python3.7-venv \
python3.8 python3.8-dev python3.8-venv \
python3.9 python3.9-dev python3.9-venv && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN useradd -mU dbt_test_user
RUN mkdir /usr/app && chown dbt_test_user /usr/app
RUN mkdir /home/tox && chown dbt_test_user /home/tox
WORKDIR /usr/app
VOLUME /usr/app
RUN pip3 install -U "tox==3.14.4" wheel "six>=1.14.0,<1.15.0" "virtualenv==20.0.3" setuptools
# tox fails if the 'python' interpreter (python2) doesn't have `tox` installed
RUN pip install -U "tox==3.14.4" "six>=1.14.0,<1.15.0" "virtualenv==20.0.3" setuptools
USER dbt_test_user
ENV PYTHONIOENCODING=utf-8
ENV LANG C.UTF-8

75
Dockerfile.test Normal file
View File

@@ -0,0 +1,75 @@
FROM ubuntu:20.04
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
software-properties-common \
&& add-apt-repository ppa:git-core/ppa -y \
&& apt-get dist-upgrade -y \
&& apt-get install -y --no-install-recommends \
netcat \
postgresql \
curl \
git \
ssh \
software-properties-common \
make \
build-essential \
ca-certificates \
libpq-dev \
libsasl2-dev \
libsasl2-2 \
libsasl2-modules-gssapi-mit \
libyaml-dev \
unixodbc-dev \
&& add-apt-repository ppa:deadsnakes/ppa \
&& apt-get install -y \
python \
python-dev \
python-pip \
python3.6 \
python3.6-dev \
python3-pip \
python3.6-venv \
python3.7 \
python3.7-dev \
python3.7-venv \
python3.8 \
python3.8-dev \
python3.8-venv \
python3.9 \
python3.9-dev \
python3.9-venv \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
ARG DOCKERIZE_VERSION=v0.6.1
RUN curl -LO https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
RUN pip3 install -U tox wheel six setuptools
# These args are passed in via docker-compose, which reads then from the .env file.
# On Linux, run `make .env` to create the .env file for the current user.
# On MacOS and Windows, these can stay unset.
ARG USER_ID
ARG GROUP_ID
RUN if [ ${USER_ID:-0} -ne 0 ] && [ ${GROUP_ID:-0} -ne 0 ]; then \
groupadd -g ${GROUP_ID} dbt_test_user && \
useradd -m -l -u ${USER_ID} -g ${GROUP_ID} dbt_test_user; \
else \
useradd -mU -l dbt_test_user; \
fi
RUN mkdir /usr/app && chown dbt_test_user /usr/app
RUN mkdir /home/tox && chown dbt_test_user /home/tox
WORKDIR /usr/app
VOLUME /usr/app
USER dbt_test_user
ENV PYTHONIOENCODING=utf-8
ENV LANG C.UTF-8

View File

@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Copyright 2021 dbt Labs, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

110
Makefile
View File

@@ -1,29 +1,94 @@
.PHONY: install test test-unit test-integration
.DEFAULT_GOAL:=help
changed_tests := `git status --porcelain | grep '^\(M\| M\|A\| A\)' | awk '{ print $$2 }' | grep '\/test_[a-zA-Z_\-\.]\+.py'`
# Optional flag to run target in a docker container.
# (example `make test USE_DOCKER=true`)
ifeq ($(USE_DOCKER),true)
DOCKER_CMD := docker-compose run --rm test
endif
install:
pip install -e .
.PHONY: dev
dev: ## Installs dbt-* packages in develop mode along with development dependencies.
pip install -r dev-requirements.txt -r editable-requirements.txt
test:
@echo "Full test run starting..."
@time docker-compose run test tox
.PHONY: mypy
mypy: .env ## Runs mypy for static type checking.
$(DOCKER_CMD) tox -e mypy
test-unit:
@echo "Unit test run starting..."
@time docker-compose run test tox -e unit-py36,flake8
.PHONY: flake8
flake8: .env ## Runs flake8 to enforce style guide.
$(DOCKER_CMD) tox -e flake8
test-integration:
@echo "Integration test run starting..."
@time docker-compose run test tox -e integration-postgres-py36integration-redshift-py36,integration-snowflake-py36,integration-bigquery-py36
.PHONY: lint
lint: .env ## Runs all code checks in parallel.
$(DOCKER_CMD) tox -p -e flake8,mypy
test-quick:
@echo "Integration test run starting..."
@time docker-compose run test tox -e integration-postgres-py36 -- -x
.PHONY: unit
unit: .env ## Runs unit tests with py38.
$(DOCKER_CMD) tox -e py38
clean:
.PHONY: test
test: .env ## Runs unit tests with py38 and code checks in parallel.
$(DOCKER_CMD) tox -p -e py38,flake8,mypy
.PHONY: integration
integration: .env integration-postgres ## Alias for integration-postgres.
.PHONY: integration-fail-fast
integration-fail-fast: .env integration-postgres-fail-fast ## Alias for integration-postgres-fail-fast.
.PHONY: integration-postgres
integration-postgres: .env ## Runs postgres integration tests with py38.
$(DOCKER_CMD) tox -e py38-postgres -- -nauto
.PHONY: integration-postgres-fail-fast
integration-postgres-fail-fast: .env ## Runs postgres integration tests with py38 in "fail fast" mode.
$(DOCKER_CMD) tox -e py38-postgres -- -x -nauto
.PHONY: integration-redshift
integration-redshift: .env ## Runs redshift integration tests with py38.
$(DOCKER_CMD) tox -e py38-redshift -- -nauto
.PHONY: integration-redshift-fail-fast
integration-redshift-fail-fast: .env ## Runs redshift integration tests with py38 in "fail fast" mode.
$(DOCKER_CMD) tox -e py38-redshift -- -x -nauto
.PHONY: integration-snowflake
integration-snowflake: .env ## Runs snowflake integration tests with py38.
$(DOCKER_CMD) tox -e py38-snowflake -- -nauto
.PHONY: integration-snowflake-fail-fast
integration-snowflake-fail-fast: .env ## Runs snowflake integration tests with py38 in "fail fast" mode.
$(DOCKER_CMD) tox -e py38-snowflake -- -x -nauto
.PHONY: integration-bigquery
integration-bigquery: .env ## Runs bigquery integration tests with py38.
$(DOCKER_CMD) tox -e py38-bigquery -- -nauto
.PHONY: integration-bigquery-fail-fast
integration-bigquery-fail-fast: .env ## Runs bigquery integration tests with py38 in "fail fast" mode.
$(DOCKER_CMD) tox -e py38-bigquery -- -x -nauto
.PHONY: setup-db
setup-db: ## Setup Postgres database with docker-compose for system testing.
docker-compose up -d database
PGHOST=localhost PGUSER=root PGPASSWORD=password PGDATABASE=postgres bash test/setup_db.sh
# This rule creates a file named .env that is used by docker-compose for passing
# the USER_ID and GROUP_ID arguments to the Docker image.
.env: ## Setup step for using using docker-compose with make target.
@touch .env
ifneq ($(OS),Windows_NT)
ifneq ($(shell uname -s), Darwin)
@echo USER_ID=$(shell id -u) > .env
@echo GROUP_ID=$(shell id -g) >> .env
endif
endif
.PHONY: clean
clean: ## Resets development environment.
rm -f .coverage
rm -rf .eggs/
rm -f .env
rm -rf .tox/
rm -rf build/
rm -rf dbt.egg-info/
@@ -34,3 +99,14 @@ clean:
rm -rf target/
find . -type f -name '*.pyc' -delete
find . -type d -name '__pycache__' -depth -delete
.PHONY: help
help: ## Show this help message.
@echo 'usage: make [target] [USE_DOCKER=true]'
@echo
@echo 'targets:'
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
@echo
@echo 'options:'
@echo 'use USE_DOCKER=true to run target in a docker container'

View File

@@ -1,28 +1,18 @@
<p align="center">
<img src="https://github.com/fishtown-analytics/dbt/blob/master/etc/dbt-horizontal.png?raw=true" alt="dbt logo"/>
<img src="https://raw.githubusercontent.com/dbt-labs/dbt/ec7dee39f793aa4f7dd3dae37282cc87664813e4/etc/dbt-logo-full.svg" alt="dbt logo" width="500"/>
</p>
<p align="center">
<a href="https://codeclimate.com/github/fishtown-analytics/dbt">
<img src="https://codeclimate.com/github/fishtown-analytics/dbt/badges/gpa.svg" alt="Code Climate"/>
<a href="https://github.com/dbt-labs/dbt/actions/workflows/main.yml">
<img src="https://github.com/dbt-labs/dbt/actions/workflows/main.yml/badge.svg?event=push" alt="Unit Tests Badge"/>
</a>
<a href="https://circleci.com/gh/fishtown-analytics/dbt/tree/master">
<img src="https://circleci.com/gh/fishtown-analytics/dbt/tree/master.svg?style=svg" alt="CircleCI" />
</a>
<a href="https://ci.appveyor.com/project/DrewBanin/dbt/branch/development">
<img src="https://ci.appveyor.com/api/projects/status/v01rwd3q91jnwp9m/branch/development?svg=true" alt="AppVeyor" />
</a>
<a href="https://slack.getdbt.com">
<img src="https://slack.getdbt.com/badge.svg" alt="Slack" />
<a href="https://github.com/dbt-labs/dbt/actions/workflows/integration.yml">
<img src="https://github.com/dbt-labs/dbt/actions/workflows/integration.yml/badge.svg?event=push" alt="Integration Tests Badge"/>
</a>
</p>
**[dbt](https://www.getdbt.com/)** (data build tool) 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.
dbt is the T in ELT. Organize, cleanse, denormalize, filter, rename, and pre-aggregate the raw data in your warehouse so that it's ready for analysis.
![dbt architecture](https://github.com/fishtown-analytics/dbt/blob/master/etc/dbt-arch.png?raw=true)
dbt can be used to [aggregate pageviews into sessions](https://github.com/fishtown-analytics/snowplow), calculate [ad spend ROI](https://github.com/fishtown-analytics/facebook-ads), or report on [email campaign performance](https://github.com/fishtown-analytics/mailchimp).
![architecture](https://raw.githubusercontent.com/dbt-labs/dbt/6c6649f9129d5d108aa3b0526f634cd8f3a9d1ed/etc/dbt-arch.png)
## Understanding dbt
@@ -30,29 +20,23 @@ Analysts using dbt can transform their data by simply writing select statements,
These select statements, or "models", form a dbt project. Models frequently build on top of one another dbt makes it easy to [manage relationships](https://docs.getdbt.com/docs/ref) between models, and [visualize these relationships](https://docs.getdbt.com/docs/documentation), as well as assure the quality of your transformations through [testing](https://docs.getdbt.com/docs/testing).
![dbt dag](https://github.com/fishtown-analytics/dbt/blob/master/etc/dbt-dag.png?raw=true)
![dbt dag](https://raw.githubusercontent.com/dbt-labs/dbt/6c6649f9129d5d108aa3b0526f634cd8f3a9d1ed/etc/dbt-dag.png)
## Getting started
- [Install dbt](https://docs.getdbt.com/docs/installation)
- Read the [documentation](https://docs.getdbt.com/).
- Productionize your dbt project with [dbt Cloud](https://www.getdbt.com)
- [Install dbt](https://docs.getdbt.com/docs/installation)
- Read the [introduction](https://docs.getdbt.com/docs/introduction/) and [viewpoint](https://docs.getdbt.com/docs/about/viewpoint/)
## Find out more
## Join the dbt Community
- Check out the [Introduction to dbt](https://dbt.readme.io/docs/introduction).
- Read the [dbt Viewpoint](https://dbt.readme.io/docs/viewpoint).
## Join thousands of analysts in the dbt community
- Join the [chat](http://slack.getdbt.com/) on Slack.
- Find community posts on [dbt Discourse](https://discourse.getdbt.com).
- Be part of the conversation in the [dbt Community Slack](http://community.getdbt.com/)
- Read more on the [dbt Community Discourse](https://discourse.getdbt.com)
## Reporting bugs and contributing code
- Want to report a bug or request a feature? Let us know on [Slack](http://slack.getdbt.com/), or open [an issue](https://github.com/fishtown-analytics/dbt/issues/new).
- Want to help us build dbt? Check out the [Contributing Getting Started Guide](/CONTRIBUTING.md)
- Want to report a bug or request a feature? Let us know on [Slack](http://community.getdbt.com/), or open [an issue](https://github.com/dbt-labs/dbt/issues/new)
- Want to help us build dbt? Check out the [Contributing Guide](https://github.com/dbt-labs/dbt/blob/HEAD/CONTRIBUTING.md)
## Code of Conduct
Everyone interacting in the dbt project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/).
Everyone interacting in the dbt project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [dbt Code of Conduct](https://community.getdbt.com/code-of-conduct).

View File

@@ -1,92 +0,0 @@
### Release Procedure :shipit:
#### Branching Strategy
dbt has three types of branches:
- **Trunks** track the latest release of a minor version of dbt. Historically, we used the `master` branch as the trunk. Each minor version release has a corresponding trunk. 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.
- **Release Branches** track a specific, not yet complete release of dbt. These releases are codenamed since we don't always know what their semantic version will be. Example: `dev/lucretia-mott` became `0.11.1`.
- **Feature Branches** track individual features and fixes. On completion they should be merged into a release branch.
#### Git & PyPI
1. Update CHANGELOG.md with the most recent changes
2. If this is a release candidate, you want to create it off of your release branch. If it's an actual release, you must first merge to a master branch. Open a Pull Request in Github to merge it into the appropriate trunk (`X.X.latest`)
3. Bump the version using `bumpversion`:
- Dry run first by running `bumpversion --new-version <desired-version> <part>` and checking the diff. If it looks correct, clean up the chanages and move on:
- Alpha releases: `bumpversion --commit --no-tag --new-version 0.10.2a1 num`
- Patch releases: `bumpversion --commit --no-tag --new-version 0.10.2 patch`
- Minor releases: `bumpversion --commit --no-tag --new-version 0.11.0 minor`
- Major releases: `bumpversion --commit --no-tag --new-version 1.0.0 major`
4. (If this is a not a release candidate) Merge to `x.x.latest` and (optionally) `master`.
5. Update the default branch to the next dev release branch.
6. Build source distributions for all packages by running `./scripts/build-sdists.sh`. Note that this will clean out your `dist/` folder, so if you have important stuff in there, don't run it!!!
7. Deploy to pypi
- `twine upload dist/*`
8. Deploy to homebrew (see below)
9. Deploy to conda-forge (see below)
10. Git release notes (points to changelog)
11. Post to slack (point to changelog)
After releasing a new version, it's important to merge the changes back into the other outstanding release branches. This avoids merge conflicts moving forward.
In some cases, where the branches have diverged wildly, it's ok to skip this step. But this means that the changes you just released won't be included in future releases.
#### Homebrew Release Process
1. Clone the `homebrew-dbt` repository:
```
git clone git@github.com:fishtown-analytics/homebrew-dbt.git
```
2. For ALL releases (prereleases and version releases), copy the relevant formula. To copy from the latest version release of dbt, do:
```bash
cp Formula/dbt.rb Formula/dbt@{NEW-VERSION}.rb
```
To copy from a different version, simply copy the corresponding file.
3. Open the file, and edit the following:
- the name of the ruby class: this is important, homebrew won't function properly if the class name is wrong. Check historical versions to figure out the right name.
- under the `bottle` section, remove all of the hashes (lines starting with `sha256`)
4. Create a **Python 3.7** virtualenv, activate it, and then install two packages: `homebrew-pypi-poet`, and the version of dbt you are preparing. I use:
```
pyenv virtualenv 3.7.0 homebrew-dbt-{VERSION}
pyenv activate homebrew-dbt-{VERSION}
pip install dbt=={VERSION} homebrew-pypi-poet
```
homebrew-pypi-poet is a program that generates a valid homebrew formula for an installed pip package. You want to use it to generate a diff against the existing formula. Then you want to apply the diff for the dependency packages only -- e.g. it will tell you that `google-api-core` has been updated and that you need to use the latest version.
5. reinstall, test, and audit dbt. if the test or audit fails, fix the formula with step 1.
```bash
brew uninstall --force Formula/{YOUR-FILE}.rb
brew install Formula/{YOUR-FILE}.rb
brew test dbt
brew audit --strict dbt
```
6. Ask Connor to bottle the change (only his laptop can do it!)
#### Conda Forge Release Process
1. Clone the fork of `conda-forge/dbt-feedstock` [here](https://github.com/fishtown-analytics/dbt-feedstock)
```bash
git clone git@github.com:fishtown-analytics/dbt-feedstock.git
```
2. Update the version and sha256 in `recipe/meta.yml`. To calculate the sha256, run:
```bash
wget https://github.com/fishtown-analytics/dbt/archive/v{version}.tar.gz
openssl sha256 v{version}.tar.gz
```
3. Push the changes and create a PR against `conda-forge/dbt-feedstock`
4. Confirm that all automated conda-forge tests are passing

View File

@@ -1,158 +0,0 @@
# Python package
# Create and test a Python package on multiple Python versions.
# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/python
trigger:
branches:
include:
- master
- dev/*
- pr/*
jobs:
- job: UnitTest
pool:
vmImage: 'vs2017-win2016'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.7'
architecture: 'x64'
- script: python -m pip install --upgrade pip && pip install tox
displayName: 'Install dependencies'
- script: python -m tox -e pywin-unit
displayName: Run unit tests
- job: PostgresIntegrationTest
pool:
vmImage: 'vs2017-win2016'
dependsOn: UnitTest
steps:
- pwsh: echo "True"
displayName: Skip everything
- pwsh: |
choco install postgresql --params '/Password:password' --params-global --version 10.6
Set-Content "c:\program files\postgresql\10\data\pg_hba.conf" "host all all ::1/128 trust"
Add-Content "c:\program files\postgresql\10\data\pg_hba.conf" "host all all 127.0.0.1/32 trust"
# the service name is "postgresql-x64-10", conveniently it's both the display name and the actual name
Restart-Service postgresql-x64-10
& "C:\program files\postgresql\10\bin\createdb.exe" -U postgres dbt
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "CREATE ROLE root WITH PASSWORD 'password';"
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "ALTER ROLE root WITH LOGIN;"
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "GRANT CREATE, CONNECT ON DATABASE dbt TO root WITH GRANT OPTION;"
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "CREATE ROLE noaccess WITH PASSWORD 'password' NOSUPERUSER;"
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "ALTER ROLE noaccess WITH LOGIN;"
& "C:\program files\postgresql\10\bin\psql.exe" -U postgres -c "GRANT CONNECT ON DATABASE dbt TO noaccess;"
displayName: Install postgresql and set up database
- task: UsePythonVersion@0
inputs:
versionSpec: '3.7'
architecture: 'x64'
- script: python -m pip install --upgrade pip && pip install tox
displayName: 'Install dependencies'
- script: python -m tox -e pywin-postgres
displayName: Run integration tests
# These three are all similar except secure environment variables, which MUST be passed along to their tasks,
# but there's probably a better way to do this!
- job: SnowflakeIntegrationTest
pool:
vmImage: 'vs2017-win2016'
dependsOn: PostgresIntegrationTest
condition: and(succeeded(), ne(variables['SYSTEM.PULLREQUEST.ISFORK'], 'true'))
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.7'
architecture: 'x64'
- script: python -m pip install --upgrade pip && pip install tox
displayName: 'Install dependencies'
- script: python -m tox -e pywin-snowflake
env:
SNOWFLAKE_TEST_ACCOUNT: $(SNOWFLAKE_TEST_ACCOUNT)
SNOWFLAKE_TEST_PASSWORD: $(SNOWFLAKE_TEST_PASSWORD)
SNOWFLAKE_TEST_USER: $(SNOWFLAKE_TEST_USER)
SNOWFLAKE_TEST_WAREHOUSE: $(SNOWFLAKE_TEST_WAREHOUSE)
SNOWFLAKE_TEST_OAUTH_REFRESH_TOKEN: $(SNOWFLAKE_TEST_OAUTH_REFRESH_TOKEN)
SNOWFLAKE_TEST_OAUTH_CLIENT_ID: $(SNOWFLAKE_TEST_OAUTH_CLIENT_ID)
SNOWFLAKE_TEST_OAUTH_CLIENT_SECRET: $(SNOWFLAKE_TEST_OAUTH_CLIENT_SECRET)
displayName: Run integration tests
- job: BigQueryIntegrationTest
pool:
vmImage: 'vs2017-win2016'
dependsOn: PostgresIntegrationTest
condition: and(succeeded(), ne(variables['SYSTEM.PULLREQUEST.ISFORK'], 'true'))
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.7'
architecture: 'x64'
- script: python -m pip install --upgrade pip && pip install tox
displayName: 'Install dependencies'
- script: python -m tox -e pywin-bigquery
env:
BIGQUERY_SERVICE_ACCOUNT_JSON: $(BIGQUERY_SERVICE_ACCOUNT_JSON)
displayName: Run integration tests
- job: RedshiftIntegrationTest
pool:
vmImage: 'vs2017-win2016'
dependsOn: PostgresIntegrationTest
condition: and(succeeded(), ne(variables['SYSTEM.PULLREQUEST.ISFORK'], 'true'))
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.7'
architecture: 'x64'
- script: python -m pip install --upgrade pip && pip install tox
displayName: 'Install dependencies'
- script: python -m tox -e pywin-redshift
env:
REDSHIFT_TEST_DBNAME: $(REDSHIFT_TEST_DBNAME)
REDSHIFT_TEST_PASS: $(REDSHIFT_TEST_PASS)
REDSHIFT_TEST_USER: $(REDSHIFT_TEST_USER)
REDSHIFT_TEST_PORT: $(REDSHIFT_TEST_PORT)
REDSHIFT_TEST_HOST: $(REDSHIFT_TEST_HOST)
displayName: Run integration tests
- job: BuildWheel
pool:
vmImage: 'vs2017-win2016'
dependsOn:
- UnitTest
- PostgresIntegrationTest
- RedshiftIntegrationTest
- SnowflakeIntegrationTest
- BigQueryIntegrationTest
condition: and(succeeded(), ne(variables['SYSTEM.PULLREQUEST.ISFORK'], 'true'))
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.7'
architecture: 'x64'
- script: python -m pip install --upgrade pip setuptools && python -m pip install -r requirements.txt && python -m pip install -r dev_requirements.txt
displayName: Install dependencies
- task: ShellScript@2
inputs:
scriptPath: scripts/build-wheels.sh
- task: CopyFiles@2
inputs:
contents: 'dist\?(*.whl|*.tar.gz)'
TargetFolder: '$(Build.ArtifactStagingDirectory)'
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: dists

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env python
import json
import yaml
import sys
import argparse
from datetime import datetime, timezone
import dbt.clients.registry as registry
def yaml_type(fname):
with open(fname) as f:
return yaml.load(f)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--project", type=yaml_type, default="dbt_project.yml")
parser.add_argument("--namespace", required=True)
return parser.parse_args()
def get_full_name(args):
return "{}/{}".format(args.namespace, args.project["name"])
def init_project_in_packages(args, packages):
full_name = get_full_name(args)
if full_name not in packages:
packages[full_name] = {
"name": args.project["name"],
"namespace": args.namespace,
"latest": args.project["version"],
"assets": {},
"versions": {},
}
return packages[full_name]
def add_version_to_package(args, project_json):
project_json["versions"][args.project["version"]] = {
"id": "{}/{}".format(get_full_name(args), args.project["version"]),
"name": args.project["name"],
"version": args.project["version"],
"description": "",
"published_at": datetime.now(timezone.utc).astimezone().isoformat(),
"packages": args.project.get("packages") or [],
"works_with": [],
"_source": {
"type": "github",
"url": "",
"readme": "",
},
"downloads": {
"tarball": "",
"format": "tgz",
"sha1": "",
},
}
def main():
args = parse_args()
packages = registry.packages()
project_json = init_project_in_packages(args, packages)
if args.project["version"] in project_json["versions"]:
raise Exception("Version {} already in packages JSON"
.format(args.project["version"]),
file=sys.stderr)
add_version_to_package(args, project_json)
print(json.dumps(packages, indent=2))
if __name__ == "__main__":
main()

View File

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

View File

@@ -4,7 +4,11 @@
from dbt.contracts.connection import Credentials # noqa
from dbt.adapters.base.meta import available # noqa
from dbt.adapters.base.connections import BaseConnectionManager # noqa
from dbt.adapters.base.relation import BaseRelation, RelationType # noqa
from dbt.adapters.base.relation import ( # noqa
BaseRelation,
RelationType,
SchemaSearchMap,
)
from dbt.adapters.base.column import Column # noqa
from dbt.adapters.base.impl import BaseAdapter # noqa
from dbt.adapters.base.impl import AdapterConfig, BaseAdapter # noqa
from dbt.adapters.base.plugin import AdapterPlugin # noqa

View File

@@ -1,12 +1,12 @@
from dataclasses import dataclass
from hologram import JsonSchemaMixin
import re
from typing import Dict, ClassVar, Any, Optional
from dbt.exceptions import RuntimeException
@dataclass
class Column(JsonSchemaMixin):
class Column:
TYPE_LABELS: ClassVar[Dict[str, str]] = {
'STRING': 'TEXT',
'TIMESTAMP': 'TIMESTAMP',
@@ -74,7 +74,7 @@ class Column(JsonSchemaMixin):
def string_size(self) -> int:
if not self.is_string():
raise RuntimeError("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:
# char_size should never be None. Handle it reasonably just in case
@@ -108,3 +108,46 @@ class Column(JsonSchemaMixin):
def __repr__(self) -> str:
return "<Column {} ({})>".format(self.name, self.data_type)
@classmethod
def from_description(cls, name: str, raw_data_type: str) -> 'Column':
match = re.match(r'([^(]+)(\([^)]+\))?', raw_data_type)
if match is None:
raise RuntimeException(
f'Could not interpret data type "{raw_data_type}"'
)
data_type, size_info = match.groups()
char_size = None
numeric_precision = None
numeric_scale = None
if size_info is not None:
# strip out the parentheses
size_info = size_info[1:-1]
parts = size_info.split(',')
if len(parts) == 1:
try:
char_size = int(parts[0])
except ValueError:
raise RuntimeException(
f'Could not interpret data_type "{raw_data_type}": '
f'could not convert "{parts[0]}" to an integer'
)
elif len(parts) == 2:
try:
numeric_precision = int(parts[0])
except ValueError:
raise RuntimeException(
f'Could not interpret data_type "{raw_data_type}": '
f'could not convert "{parts[0]}" to an integer'
)
try:
numeric_scale = int(parts[1])
except ValueError:
raise RuntimeException(
f'Could not interpret data_type "{raw_data_type}": '
f'could not convert "{parts[1]}" to an integer'
)
return cls(
name, data_type, char_size, numeric_precision, numeric_scale
)

View File

@@ -4,21 +4,22 @@ import os
from multiprocessing.synchronize import RLock
from threading import get_ident
from typing import (
Dict, Tuple, Hashable, Optional, ContextManager, List
Dict, Tuple, Hashable, Optional, ContextManager, List, Union
)
import agate
import dbt.exceptions
import dbt.flags
from dbt.contracts.connection import (
Connection, Identifier, ConnectionState, AdapterRequiredConfig, LazyHandle
Connection, Identifier, ConnectionState,
AdapterRequiredConfig, LazyHandle, AdapterResponse
)
from dbt.contracts.graph.manifest import Manifest
from dbt.adapters.base.query_headers import (
MacroQueryStringSetter,
)
from dbt.logger import GLOBAL_LOGGER as logger
from dbt import flags
class BaseConnectionManager(metaclass=abc.ABCMeta):
@@ -39,7 +40,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
def __init__(self, profile: AdapterRequiredConfig):
self.profile = profile
self.thread_connections: Dict[Hashable, Connection] = {}
self.lock: RLock = dbt.flags.MP_CONTEXT.RLock()
self.lock: RLock = flags.MP_CONTEXT.RLock()
self.query_header: Optional[MacroQueryStringSetter] = None
def set_query_header(self, manifest: Manifest) -> None:
@@ -60,7 +61,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
)
return self.thread_connections[key]
def set_thread_connection(self, conn):
def set_thread_connection(self, conn: Connection) -> None:
key = self.get_thread_identifier()
if key in self.thread_connections:
raise dbt.exceptions.InternalException(
@@ -88,6 +89,11 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
self.begin()
self.commit()
def rollback_if_open(self) -> None:
conn = self.get_if_exists()
if conn is not None and conn.handle and conn.transaction_open:
self._rollback(conn)
@abc.abstractmethod
def exception_handler(self, sql: str) -> ContextManager:
"""Create a context manager that handles exceptions caused by database
@@ -139,10 +145,6 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
.format(conn.name)
)
else:
logger.debug(
'Opening a new connection, currently in state {}'
.format(conn.state)
)
conn.handle = LazyHandle(self.open)
conn.name = conn_name
@@ -176,11 +178,9 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
return
try:
if conn.state == 'open':
if conn.transaction_open is True:
self._rollback(conn)
else:
self.close(conn)
# always close the connection. close() calls _rollback() if there
# is an open transaction
self.close(conn)
except Exception:
# if rollback or close failed, remove our busted connection
self.clear_thread_connection()
@@ -230,16 +230,15 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
"""Perform the actual close operation."""
# On windows, sometimes connection handles don't have a close() attr.
if hasattr(connection.handle, 'close'):
logger.debug('On {}: Close'.format(connection.name))
logger.debug(f'On {connection.name}: Close')
connection.handle.close()
else:
logger.debug('On {}: No close available on handle'
.format(connection.name))
logger.debug(f'On {connection.name}: No close available on handle')
@classmethod
def _rollback(cls, connection: Connection) -> None:
"""Roll back the given connection."""
if dbt.flags.STRICT_MODE:
if flags.STRICT_MODE:
if not isinstance(connection, Connection):
raise dbt.exceptions.CompilerException(
f'In _rollback, got {connection} - not a Connection!'
@@ -247,17 +246,18 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
if connection.transaction_open is False:
raise dbt.exceptions.InternalException(
'Tried to rollback transaction on connection "{}", but '
'it does not have one open!'.format(connection.name))
f'Tried to rollback transaction on connection '
f'"{connection.name}", but it does not have one open!'
)
logger.debug('On {}: ROLLBACK'.format(connection.name))
logger.debug(f'On {connection.name}: ROLLBACK')
cls._rollback_handle(connection)
connection.transaction_open = False
@classmethod
def close(cls, connection: Connection) -> Connection:
if dbt.flags.STRICT_MODE:
if flags.STRICT_MODE:
if not isinstance(connection, Connection):
raise dbt.exceptions.CompilerException(
f'In close, got {connection} - not a Connection!'
@@ -268,6 +268,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
return connection
if connection.transaction_open and connection.handle:
logger.debug('On {}: ROLLBACK'.format(connection.name))
cls._rollback_handle(connection)
connection.transaction_open = False
@@ -290,7 +291,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
@abc.abstractmethod
def execute(
self, sql: str, auto_begin: bool = False, fetch: bool = False
) -> Tuple[str, agate.Table]:
) -> Tuple[Union[str, AdapterResponse], agate.Table]:
"""Execute the given SQL.
:param str sql: The sql to execute.
@@ -298,7 +299,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
transaction, automatically begin one.
:param bool fetch: If set, fetch results.
:return: A tuple of the status and the results (empty if fetch=False).
:rtype: Tuple[str, agate.Table]
:rtype: Tuple[Union[str, AdapterResponse], agate.Table]
"""
raise dbt.exceptions.NotImplementedException(
'`execute` is not implemented for this adapter!'

View File

@@ -1,11 +1,11 @@
import abc
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import Future # noqa - we use this for typing only
from concurrent.futures import as_completed, Future
from contextlib import contextmanager
from datetime import datetime
from itertools import chain
from typing import (
Optional, Tuple, Callable, Container, FrozenSet, Type, Dict, Any, List,
Mapping, Iterator, Union, Set
Optional, Tuple, Callable, Iterable, Type, Dict, Any, List, Mapping,
Iterator, Union, Set
)
import agate
@@ -16,23 +16,28 @@ from dbt.exceptions import (
get_relation_returned_multiple_results,
InternalException, NotImplementedException, RuntimeException,
)
import dbt.flags
from dbt import flags
from dbt import deprecations
from dbt.clients.agate_helper import empty_table, merge_tables
from dbt.adapters.protocol import (
AdapterConfig,
ConnectionManagerProtocol,
)
from dbt.clients.agate_helper import empty_table, merge_tables, table_from_rows
from dbt.clients.jinja import MacroGenerator
from dbt.contracts.graph.compiled import CompileResultNode, CompiledSeedNode
from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.graph.compiled import (
CompileResultNode, CompiledSeedNode
)
from dbt.contracts.graph.manifest import Manifest, MacroManifest
from dbt.contracts.graph.parsed import ParsedSeedNode
from dbt.exceptions import warn_or_error
from dbt.node_types import NodeType
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.utils import filter_null_values
from dbt.utils import filter_null_values, executor
from dbt.adapters.base.connections import BaseConnectionManager, Connection
from dbt.adapters.base.connections import Connection, AdapterResponse
from dbt.adapters.base.meta import AdapterMeta, available
from dbt.adapters.base.relation import (
ComponentName, BaseRelation, InformationSchema
ComponentName, BaseRelation, InformationSchema, SchemaSearchMap
)
from dbt.adapters.base import Column as BaseColumn
from dbt.adapters.cache import RelationsCache
@@ -54,21 +59,6 @@ def _expect_row_value(key: str, row: agate.Row):
return row[key]
def _relations_filter_schemas(
schemas: Container[str]
) -> Callable[[agate.Row], bool]:
def test(row):
referenced_schema = _expect_row_value('referenced_schema', row)
dependent_schema = _expect_row_value('dependent_schema', row)
# handle the null schema
if referenced_schema is not None:
referenced_schema = referenced_schema.lower()
if dependent_schema is not None:
dependent_schema = dependent_schema.lower()
return referenced_schema in schemas or dependent_schema in schemas
return test
def _catalog_filter_schemas(manifest: Manifest) -> Callable[[agate.Row], bool]:
"""Return a function that takes a row and decides if the row should be
included in the catalog output.
@@ -121,55 +111,6 @@ def _relation_name(rel: Optional[BaseRelation]) -> str:
return str(rel)
class SchemaSearchMap(Dict[InformationSchema, Set[Optional[str]]]):
"""A utility class to keep track of what information_schema tables to
search for what schemas
"""
def add(self, relation: BaseRelation):
key = relation.information_schema_only()
if key not in self:
self[key] = set()
lowered: Optional[str] = None
if relation.schema is not None:
lowered = relation.schema.lower()
self[key].add(lowered)
def search(self):
for information_schema_name, schemas in self.items():
for schema in schemas:
yield information_schema_name, schema
def schemas_searched(self):
result: Set[Tuple[str, str]] = set()
for information_schema_name, schemas in self.items():
result.update(
(information_schema_name.database, schema)
for schema in schemas
)
return result
def flatten(self):
new = self.__class__()
# make sure we don't have duplicates
seen = {r.database.lower() for r in self if r.database}
if len(seen) > 1:
raise_compiler_error(str(seen))
for information_schema_name, schema in self.search():
path = {
'database': information_schema_name.database,
'schema': schema
}
new.add(information_schema_name.incorporate(
path=path,
quote_policy={'database': False},
include_policy={'database': False},
))
return new
class BaseAdapter(metaclass=AdapterMeta):
"""The BaseAdapter provides an abstract base class for adapters.
@@ -208,17 +149,17 @@ class BaseAdapter(metaclass=AdapterMeta):
"""
Relation: Type[BaseRelation] = BaseRelation
Column: Type[BaseColumn] = BaseColumn
ConnectionManager: Type[BaseConnectionManager]
ConnectionManager: Type[ConnectionManagerProtocol]
# A set of clobber config fields accepted by this adapter
# for use in materializations
AdapterSpecificConfigs: FrozenSet[str] = frozenset()
AdapterSpecificConfigs: Type[AdapterConfig] = AdapterConfig
def __init__(self, config):
self.config = config
self.cache = RelationsCache()
self.connections = self.ConnectionManager(config)
self._internal_manifest_lazy: Optional[Manifest] = None
self._macro_manifest_lazy: Optional[MacroManifest] = None
###
# Methods that pass through to the connection manager
@@ -238,6 +179,9 @@ class BaseAdapter(metaclass=AdapterMeta):
def commit_if_has_connection(self) -> None:
self.connections.commit_if_has_connection()
def debug_query(self) -> None:
self.execute('select 1 as id')
def nice_connection_name(self) -> str:
conn = self.connections.get_if_exists()
if conn is None or conn.name is None:
@@ -268,7 +212,7 @@ class BaseAdapter(metaclass=AdapterMeta):
@available.parse(lambda *a, **k: ('', empty_table()))
def execute(
self, sql: str, auto_begin: bool = False, fetch: bool = False
) -> Tuple[str, agate.Table]:
) -> Tuple[Union[str, AdapterResponse], agate.Table]:
"""Execute the given SQL. This is a thin wrapper around
ConnectionManager.execute.
@@ -277,7 +221,7 @@ class BaseAdapter(metaclass=AdapterMeta):
transaction, automatically begin one.
:param bool fetch: If set, fetch results.
:return: A tuple of the status and the results (empty if fetch=False).
:rtype: Tuple[str, agate.Table]
:rtype: Tuple[Union[str, AdapterResponse], agate.Table]
"""
return self.connections.execute(
sql=sql,
@@ -285,6 +229,21 @@ class BaseAdapter(metaclass=AdapterMeta):
fetch=fetch
)
@available.parse(lambda *a, **k: ('', empty_table()))
def get_partitions_metadata(
self, table: str
) -> Tuple[agate.Table]:
"""Obtain partitions metadata for a BigQuery partitioned table.
:param str table_id: a partitioned table id, in standard SQL format.
:return: a partition metadata tuple, as described in
https://cloud.google.com/bigquery/docs/creating-partitioned-tables#getting_partition_metadata_using_meta_tables.
:rtype: agate.Table
"""
return self.connections.get_partitions_metadata(
table=table
)
###
# Methods that should never be overridden
###
@@ -299,32 +258,38 @@ class BaseAdapter(metaclass=AdapterMeta):
return cls.ConnectionManager.TYPE
@property
def _internal_manifest(self) -> Manifest:
if self._internal_manifest_lazy is None:
return self.load_internal_manifest()
return self._internal_manifest_lazy
def _macro_manifest(self) -> MacroManifest:
if self._macro_manifest_lazy is None:
return self.load_macro_manifest()
return self._macro_manifest_lazy
def check_internal_manifest(self) -> Optional[Manifest]:
def check_macro_manifest(self) -> Optional[MacroManifest]:
"""Return the internal manifest (used for executing macros) if it's
been initialized, otherwise return None.
"""
return self._internal_manifest_lazy
return self._macro_manifest_lazy
def load_internal_manifest(self) -> Manifest:
if self._internal_manifest_lazy is None:
def load_macro_manifest(self) -> MacroManifest:
if self._macro_manifest_lazy is None:
# avoid a circular import
from dbt.parser.manifest import load_internal_manifest
manifest = load_internal_manifest(self.config)
self._internal_manifest_lazy = manifest
return self._internal_manifest_lazy
from dbt.parser.manifest import ManifestLoader
manifest = ManifestLoader.load_macros(
self.config, self.connections.set_query_header
)
self._macro_manifest_lazy = manifest
return self._macro_manifest_lazy
def clear_macro_manifest(self):
if self._macro_manifest_lazy is not None:
self._macro_manifest_lazy = None
###
# Caching methods
###
def _schema_is_cached(self, database: str, schema: str) -> bool:
def _schema_is_cached(self, database: Optional[str], schema: str) -> bool:
"""Check if the schema is cached, and by default logs if it is not."""
if dbt.flags.USE_CACHE is False:
if flags.USE_CACHE is False:
return False
elif (database, schema) not in self.cache:
logger.debug(
@@ -335,9 +300,20 @@ class BaseAdapter(metaclass=AdapterMeta):
else:
return True
def _get_cache_schemas(
self, manifest: Manifest, exec_only: bool = False
) -> SchemaSearchMap:
def _get_cache_schemas(self, manifest: Manifest) -> Set[BaseRelation]:
"""Get the set of schema relations that the cache logic needs to
populate. This means only executable nodes are included.
"""
# the cache only cares about executable nodes
return {
self.Relation.create_from(self.config, node).without_identifier()
for node in manifest.nodes.values()
if (
node.is_relational and not node.is_ephemeral_model
)
}
def _get_catalog_schemas(self, manifest: Manifest) -> SchemaSearchMap:
"""Get a mapping of each node's "information_schema" relations to a
set of all schemas expected in that information_schema.
@@ -347,9 +323,11 @@ class BaseAdapter(metaclass=AdapterMeta):
lowercase strings.
"""
info_schema_name_map = SchemaSearchMap()
for node in manifest.nodes.values():
if exec_only and node.resource_type not in NodeType.executable():
continue
nodes: Iterator[CompileResultNode] = chain(
manifest.nodes.values(),
manifest.sources.values(),
)
for node in nodes:
relation = self.Relation.create_from(self.config, node)
info_schema_name_map.add(relation)
# result is a map whose keys are information_schema Relations without
@@ -362,19 +340,34 @@ class BaseAdapter(metaclass=AdapterMeta):
"""Populate the relations cache for the given schemas. Returns an
iterable of the schemas populated, as strings.
"""
if not dbt.flags.USE_CACHE:
if not flags.USE_CACHE:
return
info_schema_name_map = self._get_cache_schemas(manifest,
exec_only=True)
for db, schema in info_schema_name_map.search():
for relation in self.list_relations_without_caching(db, schema):
self.cache.add(relation)
cache_schemas = self._get_cache_schemas(manifest)
with executor(self.config) as tpe:
futures: List[Future[List[BaseRelation]]] = []
for cache_schema in cache_schemas:
fut = tpe.submit_connected(
self,
f'list_{cache_schema.database}_{cache_schema.schema}',
self.list_relations_without_caching,
cache_schema
)
futures.append(fut)
for future in as_completed(futures):
# if we can't read the relations we need to just raise anyway,
# so just call future.result() and let that raise on failure
for relation in future.result():
self.cache.add(relation)
# it's possible that there were no relations in some schemas. We want
# to insert the schemas we query into the cache's `.schemas` attribute
# so we can check it later
self.cache.update_schemas(info_schema_name_map.schemas_searched())
cache_update: Set[Tuple[Optional[str], Optional[str]]] = set()
for relation in cache_schemas:
cache_update.add((relation.database, relation.schema))
self.cache.update_schemas(cache_update)
def set_relations_cache(
self, manifest: Manifest, clear: bool = False
@@ -382,7 +375,7 @@ class BaseAdapter(metaclass=AdapterMeta):
"""Run a query that gets a populated cache of the relations in the
database and set the cache on this adapter.
"""
if not dbt.flags.USE_CACHE:
if not flags.USE_CACHE:
return
with self.cache.lock:
@@ -398,7 +391,7 @@ class BaseAdapter(metaclass=AdapterMeta):
raise_compiler_error(
'Attempted to cache a null relation for {}'.format(name)
)
if dbt.flags.USE_CACHE:
if flags.USE_CACHE:
self.cache.add(relation)
# so jinja doesn't render things
return ''
@@ -413,7 +406,7 @@ class BaseAdapter(metaclass=AdapterMeta):
raise_compiler_error(
'Attempted to drop a null relation for {}'.format(name)
)
if dbt.flags.USE_CACHE:
if flags.USE_CACHE:
self.cache.drop(relation)
return ''
@@ -435,7 +428,7 @@ class BaseAdapter(metaclass=AdapterMeta):
.format(src_name, dst_name, name)
)
if dbt.flags.USE_CACHE:
if flags.USE_CACHE:
self.cache.rename(from_relation, to_relation)
return ''
@@ -518,7 +511,7 @@ class BaseAdapter(metaclass=AdapterMeta):
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 NotImplementedException(
'`get_columns_in_relation` is not implemented for this adapter!'
)
@@ -553,15 +546,14 @@ class BaseAdapter(metaclass=AdapterMeta):
@abc.abstractmethod
def list_relations_without_caching(
self, information_schema: BaseRelation, schema: str
self, schema_relation: BaseRelation
) -> List[BaseRelation]:
"""List relations in the given schema, bypassing the cache.
This is used as the underlying behavior to fill the cache.
:param Relation information_schema: The information schema to list
relations from.
:param str schema: The name of the schema to list relations from.
:param schema_relation: A relation containing the database and schema
as appropraite for the underlying data warehouse
:return: The relations in schema
:rtype: List[self.Relation]
"""
@@ -673,21 +665,23 @@ class BaseAdapter(metaclass=AdapterMeta):
self.expand_column_types(from_relation, to_relation)
def list_relations(self, database: str, schema: str) -> List[BaseRelation]:
def list_relations(
self, database: Optional[str], schema: str
) -> List[BaseRelation]:
if self._schema_is_cached(database, schema):
return self.cache.get_relations(database, schema)
information_schema = self.Relation.create(
schema_relation = self.Relation.create(
database=database,
schema=schema,
identifier='',
quote_policy=self.config.quoting
).information_schema()
).without_identifier()
# we can't build the relations cache because we don't have a
# manifest so we can't run any operations.
relations = self.list_relations_without_caching(
information_schema, schema
schema_relation
)
logger.debug('with database={}, schema={}, relations={}'
@@ -768,7 +762,7 @@ class BaseAdapter(metaclass=AdapterMeta):
###
@abc.abstractmethod
@available.parse_none
def create_schema(self, database: str, schema: str):
def create_schema(self, relation: BaseRelation):
"""Create the given schema if it does not exist."""
raise NotImplementedException(
'`create_schema` is not implemented for this adapter!'
@@ -776,7 +770,7 @@ class BaseAdapter(metaclass=AdapterMeta):
@abc.abstractmethod
@available.parse_none
def drop_schema(self, database: str, schema: str):
def drop_schema(self, relation: BaseRelation):
"""Drop the given schema (and everything in it) if it exists."""
raise NotImplementedException(
'`drop_schema` is not implemented for this adapter!'
@@ -951,6 +945,7 @@ class BaseAdapter(metaclass=AdapterMeta):
context_override: Optional[Dict[str, Any]] = None,
kwargs: Dict[str, Any] = None,
release: bool = False,
text_only_columns: Optional[Iterable[str]] = None,
) -> agate.Table:
"""Look macro_name up in the manifest and execute its results.
@@ -963,15 +958,17 @@ class BaseAdapter(metaclass=AdapterMeta):
execution context.
:param kwargs: An optional dict of keyword args used to pass to the
macro.
:param release: If True, release the connection after executing.
:param release: Ignored.
"""
if release is not False:
deprecations.warn('execute-macro-release')
if kwargs is None:
kwargs = {}
if context_override is None:
context_override = {}
if manifest is None:
manifest = self._internal_manifest
manifest = self._macro_manifest
macro = manifest.find_macro_by_name(
macro_name, self.config.project_name, project
@@ -999,11 +996,8 @@ class BaseAdapter(metaclass=AdapterMeta):
macro_function = MacroGenerator(macro, macro_context)
try:
with self.connections.exception_handler(f'macro {macro_name}'):
result = macro_function(**kwargs)
finally:
if release:
self.release_connection()
return result
@classmethod
@@ -1013,6 +1007,12 @@ class BaseAdapter(metaclass=AdapterMeta):
"""Filter the table as appropriate for catalog entries. Subclasses can
override this to change filtering rules on a per-adapter basis.
"""
# force database + schema to be strings
table = table_from_rows(
table.rows,
table.column_names,
text_only_columns=['table_database', 'table_schema', 'table_name']
)
return table.where(_catalog_filter_schemas(manifest))
def _get_one_catalog(
@@ -1022,24 +1022,17 @@ class BaseAdapter(metaclass=AdapterMeta):
manifest: Manifest,
) -> agate.Table:
name = '.'.join([
str(information_schema.database),
'information_schema'
])
with self.connection_named(name):
kwargs = {
'information_schema': information_schema,
'schemas': schemas
}
table = self.execute_macro(
GET_CATALOG_MACRO_NAME,
kwargs=kwargs,
release=True,
# pass in the full manifest so we get any local project
# overrides
manifest=manifest,
)
kwargs = {
'information_schema': information_schema,
'schemas': schemas
}
table = self.execute_macro(
GET_CATALOG_MACRO_NAME,
kwargs=kwargs,
# pass in the full manifest so we get any local project
# overrides
manifest=manifest,
)
results = self._catalog_filter_table(table, manifest)
return results
@@ -1047,15 +1040,24 @@ class BaseAdapter(metaclass=AdapterMeta):
def get_catalog(
self, manifest: Manifest
) -> Tuple[agate.Table, List[Exception]]:
# snowflake is super slow. split it out into the specified threads
num_threads = self.config.threads
schema_map = self._get_cache_schemas(manifest)
schema_map = self._get_catalog_schemas(manifest)
with executor(self.config) as tpe:
futures: List[Future[agate.Table]] = []
for info, schemas in schema_map.items():
if len(schemas) == 0:
continue
name = '.'.join([
str(info.database),
'information_schema'
])
fut = tpe.submit_connected(
self, name,
self._get_one_catalog, info, schemas, manifest
)
futures.append(fut)
with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = [
executor.submit(self._get_one_catalog, info, schemas, manifest)
for info, schemas in schema_map.items() if len(schemas) > 0
]
catalogs, exceptions = catch_as_completed(futures)
return catalogs, exceptions
@@ -1082,7 +1084,6 @@ class BaseAdapter(metaclass=AdapterMeta):
table = self.execute_macro(
FRESHNESS_MACRO_NAME,
kwargs=kwargs,
release=True,
manifest=manifest
)
# now we have a 1-row table of the maximum `loaded_at_field` value and
@@ -1133,6 +1134,102 @@ class BaseAdapter(metaclass=AdapterMeta):
"""
pass
def get_compiler(self):
from dbt.compilation import Compiler
return Compiler(self.config)
# Methods used in adapter tests
def update_column_sql(
self,
dst_name: str,
dst_column: str,
clause: str,
where_clause: Optional[str] = None,
) -> str:
clause = f'update {dst_name} set {dst_column} = {clause}'
if where_clause is not None:
clause += f' where {where_clause}'
return clause
def timestamp_add_sql(
self, add_to: str, number: int = 1, interval: str = 'hour'
) -> str:
# for backwards compatibility, we're compelled to set some sort of
# default. A lot of searching has lead me to believe that the
# '+ interval' syntax used in postgres/redshift is relatively common
# and might even be the SQL standard's intention.
return f"{add_to} + interval '{number} {interval}'"
def string_add_sql(
self, add_to: str, value: str, location='append',
) -> str:
if location == 'append':
return f"{add_to} || '{value}'"
elif location == 'prepend':
return f"'{value}' || {add_to}"
else:
raise RuntimeException(
f'Got an unexpected location value of "{location}"'
)
def get_rows_different_sql(
self,
relation_a: BaseRelation,
relation_b: BaseRelation,
column_names: Optional[List[str]] = None,
except_operator: str = 'EXCEPT',
) -> str:
"""Generate SQL for a query that returns a single row with a two
columns: the number of rows that are different between the two
relations and the number of mismatched rows.
"""
# This method only really exists for test reasons.
names: List[str]
if column_names is None:
columns = self.get_columns_in_relation(relation_a)
names = sorted((self.quote(c.name) for c in columns))
else:
names = sorted((self.quote(n) for n in column_names))
columns_csv = ', '.join(names)
sql = COLUMNS_EQUAL_SQL.format(
columns=columns_csv,
relation_a=str(relation_a),
relation_b=str(relation_b),
except_op=except_operator,
)
return sql
COLUMNS_EQUAL_SQL = '''
with diff_count as (
SELECT
1 as id,
COUNT(*) as num_missing FROM (
(SELECT {columns} FROM {relation_a} {except_op}
SELECT {columns} FROM {relation_b})
UNION ALL
(SELECT {columns} FROM {relation_b} {except_op}
SELECT {columns} FROM {relation_a})
) as a
), table_a as (
SELECT COUNT(*) as num_rows FROM {relation_a}
), table_b as (
SELECT COUNT(*) as num_rows FROM {relation_b}
), row_count_diff as (
select
1 as id,
table_a.num_rows - table_b.num_rows as difference
from table_a, table_b
)
select
row_count_diff.difference as row_count_difference,
diff_count.num_missing as num_mismatched
from row_count_diff
join diff_count using (id)
'''.strip()
def catch_as_completed(
futures # typing: List[Future[agate.Table]]

View File

@@ -1,7 +1,19 @@
from typing import List, Optional, Type
from dbt.adapters.base import BaseAdapter, Credentials
from dbt.adapters.base import Credentials
from dbt.exceptions import CompilationException
from dbt.adapters.protocol import AdapterProtocol
def project_name_from_path(include_path: str) -> str:
# avoid an import cycle
from dbt.config.project import Project
partial = Project.partial_load(include_path)
if partial.project_name is None:
raise CompilationException(
f'Invalid project at {include_path}: name not set!'
)
return partial.project_name
class AdapterPlugin:
@@ -13,23 +25,16 @@ class AdapterPlugin:
"""
def __init__(
self,
adapter: Type[BaseAdapter],
adapter: Type[AdapterProtocol],
credentials: Type[Credentials],
include_path: str,
dependencies: Optional[List[str]] = None
):
# avoid an import cycle
from dbt.config.project import Project
self.adapter: Type[BaseAdapter] = adapter
self.adapter: Type[AdapterProtocol] = adapter
self.credentials: Type[Credentials] = credentials
self.include_path: str = include_path
partial = Project.partial_load(include_path)
if partial.project_name is None:
raise CompilationException(
f'Invalid project at {include_path}: name not set!'
)
self.project_name: str = partial.project_name
self.project_name: str = project_name_from_path(include_path)
self.dependencies: List[str]
if dependencies is None:
self.dependencies = []

View File

@@ -3,32 +3,11 @@ from typing import Optional, Callable, Dict, Any
from dbt.clients.jinja import QueryStringGenerator
from dbt.context.configured import generate_query_header_context
from dbt.contracts.connection import AdapterRequiredConfig
from dbt.context.manifest import generate_query_header_context
from dbt.contracts.connection import AdapterRequiredConfig, QueryComment
from dbt.contracts.graph.compiled import CompileResultNode
from dbt.contracts.graph.manifest import Manifest
from dbt.exceptions import RuntimeException
from dbt.helper_types import NoValue
DEFAULT_QUERY_COMMENT = '''
{%- set comment_dict = {} -%}
{%- do comment_dict.update(
app='dbt',
dbt_version=dbt_version,
profile_name=target.get('profile_name'),
target_name=target.get('target_name'),
) -%}
{%- if node is not none -%}
{%- do comment_dict.update(
node_id=node.unique_id,
) -%}
{% else %}
{# in the node context, the connection name is the node_id #}
{%- do comment_dict.update(connection_name=connection_name) -%}
{%- endif -%}
{{ return(tojson(comment_dict)) }}
'''
class NodeWrapper:
@@ -47,14 +26,24 @@ class _QueryComment(local):
"""
def __init__(self, initial):
self.query_comment: Optional[str] = initial
self.append = False
def add(self, sql: str) -> str:
if not self.query_comment:
return sql
else:
return '/* {} */\n{}'.format(self.query_comment.strip(), sql)
def set(self, comment: Optional[str]):
if self.append:
# replace last ';' with '<comment>;'
sql = sql.rstrip()
if sql[-1] == ';':
sql = sql[:-1]
return '{}\n/* {} */;'.format(sql, self.query_comment.strip())
return '{}\n/* {} */'.format(sql, self.query_comment.strip())
return '/* {} */\n{}'.format(self.query_comment.strip(), sql)
def set(self, comment: Optional[str], append: bool):
if isinstance(comment, str) and '*/' in comment:
# tell the user "no" so they don't hurt themselves by writing
# garbage
@@ -62,6 +51,7 @@ class _QueryComment(local):
f'query comment contains illegal value "*/": {comment}'
)
self.query_comment = comment
self.append = append
QueryStringFunc = Callable[[str, Optional[NodeWrapper]], str]
@@ -87,18 +77,8 @@ class MacroQueryStringSetter:
self.comment = _QueryComment(None)
self.reset()
def _get_comment_macro(self):
if (
self.config.query_comment != NoValue() and
self.config.query_comment
):
return self.config.query_comment
# if the query comment is null/empty string, there is no comment at all
elif not self.config.query_comment:
return None
else:
# else, the default
return DEFAULT_QUERY_COMMENT
def _get_comment_macro(self) -> Optional[str]:
return self.config.query_comment.comment
def _get_context(self) -> Dict[str, Any]:
return generate_query_header_context(self.config, self.manifest)
@@ -114,4 +94,8 @@ class MacroQueryStringSetter:
if node is not None:
wrapped = NodeWrapper(node)
comment_str = self.generator(name, wrapped)
self.comment.set(comment_str)
append = False
if isinstance(self.config.query_comment, QueryComment):
append = self.config.query_comment.append
self.comment.set(comment_str, append)

View File

@@ -1,131 +1,19 @@
from dbt.utils import filter_null_values, deep_merge, classproperty
from dbt.node_types import NodeType
import dbt.exceptions
from collections.abc import Mapping, Hashable
from dataclasses import dataclass, fields
from collections.abc import Hashable
from dataclasses import dataclass
from typing import (
Optional, TypeVar, Generic, Any, Type, Dict, Union, List, Iterator, Tuple
Optional, TypeVar, Any, Type, Dict, Union, Iterator, Tuple, Set
)
from typing_extensions import Protocol
from hologram import JsonSchemaMixin
from hologram.helpers import StrEnum
from dbt.contracts.util import Replaceable
from dbt.contracts.graph.compiled import CompiledNode
from dbt.contracts.graph.parsed import ParsedSourceDefinition, ParsedNode
from dbt.contracts.relation import (
RelationType, ComponentName, HasQuoting, FakeAPIObject, Policy, Path
)
from dbt.exceptions import InternalException
from dbt import deprecations
from dbt.node_types import NodeType
from dbt.utils import filter_null_values, deep_merge, classproperty
class RelationType(StrEnum):
Table = 'table'
View = 'view'
CTE = 'cte'
MaterializedView = 'materializedview'
External = 'external'
class ComponentName(StrEnum):
Database = 'database'
Schema = 'schema'
Identifier = 'identifier'
class HasQuoting(Protocol):
quoting: Dict[str, bool]
class FakeAPIObject(JsonSchemaMixin, Replaceable, Mapping):
# override the mapping truthiness, len is always >1
def __bool__(self):
return True
def __getitem__(self, key):
try:
return getattr(self, key)
except AttributeError:
raise KeyError(key) from None
def __iter__(self):
deprecations.warn('not-a-dictionary', obj=self)
for _, name in self._get_fields():
yield name
def __len__(self):
deprecations.warn('not-a-dictionary', obj=self)
return len(fields(self.__class__))
def incorporate(self, **kwargs):
value = self.to_dict()
value = deep_merge(value, kwargs)
return self.from_dict(value)
T = TypeVar('T')
@dataclass
class _ComponentObject(FakeAPIObject, Generic[T]):
database: T
schema: T
identifier: T
def get_part(self, key: ComponentName) -> T:
if key == ComponentName.Database:
return self.database
elif key == ComponentName.Schema:
return self.schema
elif key == ComponentName.Identifier:
return self.identifier
else:
raise ValueError(
'Got a key of {}, expected one of {}'
.format(key, list(ComponentName))
)
def replace_dict(self, dct: Dict[ComponentName, T]):
kwargs: Dict[str, T] = {}
for k, v in dct.items():
kwargs[str(k)] = v
return self.replace(**kwargs)
@dataclass
class Policy(_ComponentObject[bool]):
database: bool = True
schema: bool = True
identifier: bool = True
@dataclass
class Path(_ComponentObject[Optional[str]]):
database: Optional[str]
schema: Optional[str]
identifier: Optional[str]
def __post_init__(self):
# handle pesky jinja2.Undefined sneaking in here and messing up render
if not isinstance(self.database, (type(None), str)):
raise dbt.exceptions.CompilationException(
'Got an invalid path database: {}'.format(self.database)
)
if not isinstance(self.schema, (type(None), str)):
raise dbt.exceptions.CompilationException(
'Got an invalid path schema: {}'.format(self.schema)
)
if not isinstance(self.identifier, (type(None), str)):
raise dbt.exceptions.CompilationException(
'Got an invalid path identifier: {}'.format(self.identifier)
)
def get_lowered_part(self, key: ComponentName) -> Optional[str]:
part = self.get_part(key)
if part is not None:
part = part.lower()
return part
import dbt.exceptions
Self = TypeVar('Self', bound='BaseRelation')
@@ -133,8 +21,8 @@ Self = TypeVar('Self', bound='BaseRelation')
@dataclass(frozen=True, eq=False, repr=False)
class BaseRelation(FakeAPIObject, Hashable):
type: Optional[RelationType]
path: Path
type: Optional[RelationType] = None
quote_character: str = '"'
include_policy: Policy = Policy()
quote_policy: Policy = Policy()
@@ -157,12 +45,16 @@ class BaseRelation(FakeAPIObject, Hashable):
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return self.to_dict() == other.to_dict()
return self.to_dict(omit_none=True) == other.to_dict(omit_none=True)
@classmethod
def get_default_quote_policy(cls: Type[Self]) -> Policy:
def get_default_quote_policy(cls) -> Policy:
return cls._get_field_named('quote_policy').default
@classmethod
def get_default_include_policy(cls) -> Policy:
return cls._get_field_named('include_policy').default
def get(self, key, default=None):
"""Override `.get` to return a metadata object so we don't break
dbt_utils.
@@ -254,6 +146,16 @@ class BaseRelation(FakeAPIObject, Hashable):
def information_schema_only(self) -> 'InformationSchema':
return self.information_schema()
def without_identifier(self) -> 'BaseRelation':
"""Return a form of this relation that only has the database and schema
set to included. To get the appropriately-quoted form the schema out of
the result (for use as part of a query), use `.render()`. To get the
raw database or schema name, use `.database` or `.schema`.
The hash of the returned object is the result of render().
"""
return self.include(identifier=False).replace_path(identifier=None)
def _render_iterator(
self
) -> Iterator[Tuple[Optional[ComponentName], Optional[str]]]:
@@ -267,16 +169,11 @@ class BaseRelation(FakeAPIObject, Hashable):
yield key, path_part
def render(self) -> str:
parts: List[str] = [
part for _, part in self._render_iterator() if part is not None
]
if len(parts) == 0:
raise dbt.exceptions.RuntimeException(
"No path parts are included! Nothing to render."
)
return '.'.join(parts)
# if there is nothing set, this will return the empty string.
return '.'.join(
part for _, part in self._render_iterator()
if part is not None
)
def quoted(self, identifier):
return '{quote_char}{identifier}{quote_char}'.format(
@@ -288,10 +185,10 @@ class BaseRelation(FakeAPIObject, Hashable):
def create_from_source(
cls: Type[Self], source: ParsedSourceDefinition, **kwargs: Any
) -> Self:
source_quoting = source.quoting.to_dict()
source_quoting = source.quoting.to_dict(omit_none=True)
source_quoting.pop('column', None)
quote_policy = deep_merge(
cls.get_default_quote_policy().to_dict(),
cls.get_default_quote_policy().to_dict(omit_none=True),
source_quoting,
kwargs.get('quote_policy', {}),
)
@@ -304,6 +201,23 @@ class BaseRelation(FakeAPIObject, Hashable):
**kwargs
)
@staticmethod
def add_ephemeral_prefix(name: str):
return f'__dbt__cte__{name}'
@classmethod
def create_ephemeral_from_node(
cls: Type[Self],
config: HasQuoting,
node: Union[ParsedNode, CompiledNode],
) -> Self:
# Note that ephemeral models are based on the name.
identifier = cls.add_ephemeral_prefix(node.name)
return cls.create(
type=cls.CTE,
identifier=identifier,
).quote(identifier=False)
@classmethod
def create_from_node(
cls: Type[Self],
@@ -496,3 +410,47 @@ class InformationSchema(BaseRelation):
for k, v in super()._render_iterator():
yield k, v
yield None, self.information_schema_view
class SchemaSearchMap(Dict[InformationSchema, Set[Optional[str]]]):
"""A utility class to keep track of what information_schema tables to
search for what schemas. The schema values are all lowercased to avoid
duplication.
"""
def add(self, relation: BaseRelation):
key = relation.information_schema_only()
if key not in self:
self[key] = set()
schema: Optional[str] = None
if relation.schema is not None:
schema = relation.schema.lower()
self[key].add(schema)
def search(
self
) -> Iterator[Tuple[InformationSchema, Optional[str]]]:
for information_schema_name, schemas in self.items():
for schema in schemas:
yield information_schema_name, schema
def flatten(self, allow_multiple_databases: bool = False):
new = self.__class__()
# make sure we don't have multiple databases if allow_multiple_databases is set to False
if not allow_multiple_databases:
seen = {r.database.lower() for r in self if r.database}
if len(seen) > 1:
dbt.exceptions.raise_compiler_error(str(seen))
for information_schema_name, schema in self.search():
path = {
'database': information_schema_name.database,
'schema': schema
}
new.add(information_schema_name.incorporate(
path=path,
quote_policy={'database': False},
include_policy={'database': False},
))
return new

View File

@@ -4,25 +4,20 @@ from typing import List, Iterable, Optional, Dict, Set, Tuple, Any
import threading
from dbt.logger import CACHE_LOGGER as logger
from dbt.utils import lowercase
import dbt.exceptions
_ReferenceKey = namedtuple('_ReferenceKey', 'database schema identifier')
def _lower(value: Optional[str]) -> Optional[str]:
"""Postgres schemas can be None so we can't just call lower()."""
if value is None:
return None
return value.lower()
def _make_key(relation) -> _ReferenceKey:
"""Make _ReferenceKeys with lowercase values for the cache so we don't have
to keep track of quoting
"""
return _ReferenceKey(_lower(relation.database),
_lower(relation.schema),
_lower(relation.identifier))
# databases and schemas can both be None
return _ReferenceKey(lowercase(relation.database),
lowercase(relation.schema),
lowercase(relation.identifier))
def dot_separated(key: _ReferenceKey) -> str:
@@ -53,15 +48,15 @@ class _CachedRelation:
@property
def database(self) -> Optional[str]:
return _lower(self.inner.database)
return lowercase(self.inner.database)
@property
def schema(self) -> Optional[str]:
return _lower(self.inner.schema)
return lowercase(self.inner.schema)
@property
def identifier(self) -> Optional[str]:
return _lower(self.inner.identifier)
return lowercase(self.inner.identifier)
def __copy__(self):
new = self.__class__(self.inner)
@@ -190,7 +185,7 @@ class RelationsCache:
:param database: The database name to add.
:param schema: The schema name to add.
"""
self.schemas.add((_lower(database), _lower(schema)))
self.schemas.add((lowercase(database), lowercase(schema)))
def drop_schema(
self, database: Optional[str], schema: Optional[str],
@@ -199,7 +194,7 @@ class RelationsCache:
Then remove all its contents (and their dependents, etc) as well.
"""
key = (_lower(database), _lower(schema))
key = (lowercase(database), lowercase(schema))
if key not in self.schemas:
return
@@ -217,7 +212,7 @@ class RelationsCache:
:param schemas: An iterable of the schema names to add.
"""
self.schemas.update((_lower(d), s.lower()) for (d, s) in schemas)
self.schemas.update((lowercase(d), s.lower()) for (d, s) in schemas)
def __contains__(self, schema_id: Tuple[Optional[str], str]):
"""A schema is 'in' the relations cache if it is in the set of cached
@@ -226,7 +221,7 @@ class RelationsCache:
:param schema_id: The db name and schema name to look up.
"""
db, schema = schema_id
return (_lower(db), schema.lower()) in self.schemas
return (lowercase(db), schema.lower()) in self.schemas
def dump_graph(self):
"""Dump a key-only representation of the schema to a dictionary. Every
@@ -484,13 +479,13 @@ class RelationsCache:
:return List[BaseRelation]: The list of relations with the given
schema
"""
database = _lower(database)
schema = _lower(schema)
database = lowercase(database)
schema = lowercase(schema)
with self.lock:
results = [
r.inner for r in self.relations.values()
if (_lower(r.schema) == schema and
_lower(r.database) == database)
if (lowercase(r.schema) == schema and
lowercase(r.database) == database)
]
if None in results:
@@ -509,7 +504,7 @@ class RelationsCache:
self, database: Optional[str], schema: Optional[str]
) -> List[_CachedRelation]:
"""Get the relations in a schema. Callers should hold the lock."""
key = (_lower(database), _lower(schema))
key = (lowercase(database), lowercase(schema))
to_remove: List[_CachedRelation] = []
for cachekey, relation in self.relations.items():

View File

@@ -1,47 +1,63 @@
import threading
from pathlib import Path
from importlib import import_module
from typing import Type, Dict, Any
from typing import Type, Dict, Any, List, Optional, Set
from dbt.exceptions import RuntimeException
from dbt.include.global_project import PACKAGES
from dbt.exceptions import RuntimeException, InternalException
from dbt.include.global_project import (
PACKAGE_PATH as GLOBAL_PROJECT_PATH,
PROJECT_NAME as GLOBAL_PROJECT_NAME,
)
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.contracts.connection import Credentials, AdapterRequiredConfig
from dbt.adapters.base.impl import BaseAdapter
from dbt.adapters.protocol import (
AdapterProtocol,
AdapterConfig,
RelationProtocol,
)
from dbt.adapters.base.plugin import AdapterPlugin
# TODO: we can't import these because they cause an import cycle.
# Profile has to call into load_plugin to get credentials, so adapter/relation
# don't work
BaseRelation = Any
Adapter = AdapterProtocol
Adapter = BaseAdapter
class AdpaterContainer:
class AdapterContainer:
def __init__(self):
self.lock = threading.Lock()
self.adapters: Dict[str, Adapter] = {}
self.adapter_types: Dict[str, Type[Adapter]] = {}
self.plugins: Dict[str, AdapterPlugin] = {}
# map package names to their include paths
self.packages: Dict[str, Path] = {
GLOBAL_PROJECT_NAME: Path(GLOBAL_PROJECT_PATH),
}
def get_adapter_class_by_name(self, name: str) -> Type[Adapter]:
def get_plugin_by_name(self, name: str) -> AdapterPlugin:
with self.lock:
if name in self.adapter_types:
return self.adapter_types[name]
names = ", ".join(self.adapter_types.keys())
if name in self.plugins:
return self.plugins[name]
names = ", ".join(self.plugins.keys())
message = f"Invalid adapter type {name}! Must be one of {names}"
raise RuntimeException(message)
def get_relation_class_by_name(self, name: str) -> Type[BaseRelation]:
def get_adapter_class_by_name(self, name: str) -> Type[Adapter]:
plugin = self.get_plugin_by_name(name)
return plugin.adapter
def get_relation_class_by_name(self, name: str) -> Type[RelationProtocol]:
adapter = self.get_adapter_class_by_name(name)
return adapter.Relation
def get_config_class_by_name(
self, name: str
) -> Type[AdapterConfig]:
adapter = self.get_adapter_class_by_name(name)
return adapter.AdapterSpecificConfigs
def load_plugin(self, name: str) -> Type[Credentials]:
# this doesn't need a lock: in the worst case we'll overwrite PACKAGES
# this doesn't need a lock: in the worst case we'll overwrite packages
# and adapter_type entries with the same value, as they're all
# singletons
try:
@@ -68,9 +84,9 @@ class AdpaterContainer:
with self.lock:
# things do hold the lock to iterate over it so we need it to add
self.adapter_types[name] = plugin.adapter
self.plugins[name] = plugin
PACKAGES[plugin.project_name] = plugin.include_path
self.packages[plugin.project_name] = Path(plugin.include_path)
for dep in plugin.dependencies:
self.load_plugin(dep)
@@ -108,8 +124,58 @@ class AdpaterContainer:
for adapter in self.adapters.values():
adapter.cleanup_connections()
def get_adapter_plugins(self, name: Optional[str]) -> List[AdapterPlugin]:
"""Iterate over the known adapter plugins. If a name is provided,
iterate in dependency order over the named plugin and its dependencies.
"""
if name is None:
return list(self.plugins.values())
FACTORY: AdpaterContainer = AdpaterContainer()
plugins: List[AdapterPlugin] = []
seen: Set[str] = set()
plugin_names: List[str] = [name]
while plugin_names:
plugin_name = plugin_names[0]
plugin_names = plugin_names[1:]
try:
plugin = self.plugins[plugin_name]
except KeyError:
raise InternalException(
f'No plugin found for {plugin_name}'
) from None
plugins.append(plugin)
seen.add(plugin_name)
if plugin.dependencies is None:
continue
for dep in plugin.dependencies:
if dep not in seen:
plugin_names.append(dep)
return plugins
def get_adapter_package_names(self, name: Optional[str]) -> List[str]:
package_names: List[str] = [
p.project_name for p in self.get_adapter_plugins(name)
]
package_names.append(GLOBAL_PROJECT_NAME)
return package_names
def get_include_paths(self, name: Optional[str]) -> List[Path]:
paths = []
for package_name in self.get_adapter_package_names(name):
try:
path = self.packages[package_name]
except KeyError:
raise InternalException(
f'No internal package listing found for {package_name}'
)
paths.append(path)
return paths
def get_adapter_type_names(self, name: Optional[str]) -> List[str]:
return [p.adapter.type() for p in self.get_adapter_plugins(name)]
FACTORY: AdapterContainer = AdapterContainer()
def register_adapter(config: AdapterRequiredConfig) -> None:
@@ -133,13 +199,29 @@ def cleanup_connections():
FACTORY.cleanup_connections()
def get_adapter_class_by_name(name: str) -> Type[BaseAdapter]:
def get_adapter_class_by_name(name: str) -> Type[AdapterProtocol]:
return FACTORY.get_adapter_class_by_name(name)
def get_relation_class_by_name(name: str) -> Type[BaseRelation]:
def get_config_class_by_name(name: str) -> Type[AdapterConfig]:
return FACTORY.get_config_class_by_name(name)
def get_relation_class_by_name(name: str) -> Type[RelationProtocol]:
return FACTORY.get_relation_class_by_name(name)
def load_plugin(name: str) -> Type[Credentials]:
return FACTORY.load_plugin(name)
def get_include_paths(name: Optional[str]) -> List[Path]:
return FACTORY.get_include_paths(name)
def get_adapter_package_names(name: Optional[str]) -> List[str]:
return FACTORY.get_adapter_package_names(name)
def get_adapter_type_names(name: Optional[str]) -> List[str]:
return FACTORY.get_adapter_type_names(name)

View File

@@ -0,0 +1,163 @@
from dataclasses import dataclass
from typing import (
Type, Hashable, Optional, ContextManager, List, Generic, TypeVar, ClassVar,
Tuple, Union, Dict, Any
)
from typing_extensions import Protocol
import agate
from dbt.contracts.connection import (
Connection, AdapterRequiredConfig, AdapterResponse
)
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.manifest import Manifest
from dbt.contracts.relation import Policy, HasQuoting
from dbt.graph import Graph
@dataclass
class AdapterConfig(BaseConfig):
pass
class ConnectionManagerProtocol(Protocol):
TYPE: str
class ColumnProtocol(Protocol):
pass
Self = TypeVar('Self', bound='RelationProtocol')
class RelationProtocol(Protocol):
@classmethod
def get_default_quote_policy(cls) -> Policy:
...
@classmethod
def create_from(
cls: Type[Self],
config: HasQuoting,
node: Union[CompiledNode, ParsedNode, ParsedSourceDefinition],
) -> Self:
...
class CompilerProtocol(Protocol):
def compile(self, manifest: Manifest, write=True) -> Graph:
...
def compile_node(
self,
node: ManifestNode,
manifest: Manifest,
extra_context: Optional[Dict[str, Any]] = None,
) -> NonSourceCompiledNode:
...
AdapterConfig_T = TypeVar(
'AdapterConfig_T', bound=AdapterConfig
)
ConnectionManager_T = TypeVar(
'ConnectionManager_T', bound=ConnectionManagerProtocol
)
Relation_T = TypeVar(
'Relation_T', bound=RelationProtocol
)
Column_T = TypeVar(
'Column_T', bound=ColumnProtocol
)
Compiler_T = TypeVar('Compiler_T', bound=CompilerProtocol)
class AdapterProtocol(
Protocol,
Generic[
AdapterConfig_T,
ConnectionManager_T,
Relation_T,
Column_T,
Compiler_T,
]
):
AdapterSpecificConfigs: ClassVar[Type[AdapterConfig_T]]
Column: ClassVar[Type[Column_T]]
Relation: ClassVar[Type[Relation_T]]
ConnectionManager: ClassVar[Type[ConnectionManager_T]]
connections: ConnectionManager_T
def __init__(self, config: AdapterRequiredConfig):
...
@classmethod
def type(cls) -> str:
pass
def set_query_header(self, manifest: Manifest) -> None:
...
@staticmethod
def get_thread_identifier() -> Hashable:
...
def get_thread_connection(self) -> Connection:
...
def set_thread_connection(self, conn: Connection) -> None:
...
def get_if_exists(self) -> Optional[Connection]:
...
def clear_thread_connection(self) -> None:
...
def clear_transaction(self) -> None:
...
def exception_handler(self, sql: str) -> ContextManager:
...
def set_connection_name(self, name: Optional[str] = None) -> Connection:
...
def cancel_open(self) -> Optional[List[str]]:
...
def open(cls, connection: Connection) -> Connection:
...
def release(self) -> None:
...
def cleanup_all(self) -> None:
...
def begin(self) -> None:
...
def commit(self) -> None:
...
def close(cls, connection: Connection) -> Connection:
...
def commit_if_has_connection(self) -> None:
...
def execute(
self, sql: str, auto_begin: bool = False, fetch: bool = False
) -> Tuple[Union[str, AdapterResponse], agate.Table]:
...
def get_compiler(self) -> Compiler_T:
...

View File

@@ -1,14 +1,17 @@
import abc
import time
from typing import List, Optional, Tuple, Any, Iterable, Dict
from typing import List, Optional, Tuple, Any, Iterable, Dict, Union
import agate
import dbt.clients.agate_helper
import dbt.exceptions
from dbt.contracts.connection import Connection
from dbt.adapters.base import BaseConnectionManager
from dbt.contracts.connection import (
Connection, ConnectionState, AdapterResponse
)
from dbt.logger import GLOBAL_LOGGER as logger
from dbt import flags
class SQLConnectionManager(BaseConnectionManager):
@@ -17,7 +20,7 @@ class SQLConnectionManager(BaseConnectionManager):
Methods to implement:
- exception_handler
- cancel
- get_status
- get_response
- open
"""
@abc.abstractmethod
@@ -37,7 +40,10 @@ class SQLConnectionManager(BaseConnectionManager):
# if the connection failed, the handle will be None so we have
# nothing to cancel.
if connection.handle is not None:
if (
connection.handle is not None and
connection.state == ConnectionState.OPEN
):
self.cancel(connection)
if connection.name is not None:
names.append(connection.name)
@@ -72,20 +78,19 @@ class SQLConnectionManager(BaseConnectionManager):
cursor = connection.handle.cursor()
cursor.execute(sql, bindings)
logger.debug(
"SQL status: {status} in {elapsed:0.2f} seconds",
status=self.get_status(cursor),
status=self.get_response(cursor),
elapsed=(time.time() - pre)
)
return connection, cursor
@abc.abstractclassmethod
def get_status(cls, cursor: Any) -> str:
def get_response(cls, cursor: Any) -> Union[AdapterResponse, str]:
"""Get the status of the cursor."""
raise dbt.exceptions.NotImplementedException(
'`get_status` is not implemented for this adapter!'
'`get_response` is not implemented for this adapter!'
)
@classmethod
@@ -94,7 +99,14 @@ class SQLConnectionManager(BaseConnectionManager):
column_names: Iterable[str],
rows: Iterable[Any]
) -> List[Dict[str, Any]]:
unique_col_names = dict()
for idx in range(len(column_names)):
col_name = column_names[idx]
if col_name in unique_col_names:
unique_col_names[col_name] += 1
column_names[idx] = f'{col_name}_{unique_col_names[col_name]}'
else:
unique_col_names[column_names[idx]] = 1
return [dict(zip(column_names, row)) for row in rows]
@classmethod
@@ -107,19 +119,22 @@ class SQLConnectionManager(BaseConnectionManager):
rows = cursor.fetchall()
data = cls.process_results(column_names, rows)
return dbt.clients.agate_helper.table_from_data(data, column_names)
return dbt.clients.agate_helper.table_from_data_flat(
data,
column_names
)
def execute(
self, sql: str, auto_begin: bool = False, fetch: bool = False
) -> Tuple[str, agate.Table]:
) -> Tuple[Union[AdapterResponse, str], agate.Table]:
sql = self._add_query_comment(sql)
_, cursor = self.add_query(sql, auto_begin)
status = self.get_status(cursor)
response = self.get_response(cursor)
if fetch:
table = self.get_result_from_cursor(cursor)
else:
table = dbt.clients.agate_helper.empty_table()
return status, table
return response, table
def add_begin_query(self):
return self.add_query('BEGIN', auto_begin=False)
@@ -130,7 +145,7 @@ class SQLConnectionManager(BaseConnectionManager):
def begin(self):
connection = self.get_thread_connection()
if dbt.flags.STRICT_MODE:
if flags.STRICT_MODE:
if not isinstance(connection, Connection):
raise dbt.exceptions.CompilerException(
f'In begin, got {connection} - not a Connection!'
@@ -148,7 +163,7 @@ class SQLConnectionManager(BaseConnectionManager):
def commit(self):
connection = self.get_thread_connection()
if dbt.flags.STRICT_MODE:
if flags.STRICT_MODE:
if not isinstance(connection, Connection):
raise dbt.exceptions.CompilerException(
f'In commit, got {connection} - not a Connection!'

View File

@@ -4,12 +4,11 @@ from typing import Any, Optional, Tuple, Type, List
import dbt.clients.agate_helper
from dbt.contracts.connection import Connection
import dbt.exceptions
import dbt.flags
from dbt.adapters.base import BaseAdapter, available
from dbt.adapters.sql import SQLConnectionManager
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.adapters.factory import BaseRelation
from dbt.adapters.base.relation import BaseRelation
LIST_RELATIONS_MACRO_NAME = 'list_relations_without_caching'
GET_COLUMNS_IN_RELATION_MACRO_NAME = 'get_columns_in_relation'
@@ -174,31 +173,31 @@ class SQLAdapter(BaseAdapter):
kwargs={'relation': relation}
)
def create_schema(self, database: str, schema: str) -> None:
logger.debug('Creating schema "{}"."{}".', database, schema)
def create_schema(self, relation: BaseRelation) -> None:
relation = relation.without_identifier()
logger.debug('Creating schema "{}"', relation)
kwargs = {
'database_name': self.quote_as_configured(database, 'database'),
'schema_name': self.quote_as_configured(schema, 'schema'),
'relation': relation,
}
self.execute_macro(CREATE_SCHEMA_MACRO_NAME, kwargs=kwargs)
self.commit_if_has_connection()
# we can't update the cache here, as if the schema already existed we
# don't want to (incorrectly) say that it's empty
def drop_schema(self, database: str, schema: str) -> None:
logger.debug('Dropping schema "{}"."{}".', database, schema)
def drop_schema(self, relation: BaseRelation) -> None:
relation = relation.without_identifier()
logger.debug('Dropping schema "{}".', relation)
kwargs = {
'database_name': self.quote_as_configured(database, 'database'),
'schema_name': self.quote_as_configured(schema, 'schema'),
'relation': relation,
}
self.execute_macro(DROP_SCHEMA_MACRO_NAME, kwargs=kwargs)
# we can update the cache here
self.cache.drop_schema(database, schema)
self.cache.drop_schema(relation.database, relation.schema)
def list_relations_without_caching(
self, information_schema, schema
self, schema_relation: BaseRelation,
) -> List[BaseRelation]:
kwargs = {'information_schema': information_schema, 'schema': schema}
kwargs = {'schema_relation': schema_relation}
results = self.execute_macro(
LIST_RELATIONS_MACRO_NAME,
kwargs=kwargs

View File

@@ -69,7 +69,7 @@ BLOCK_START_PATTERN = regex(''.join((
RAW_BLOCK_PATTERN = regex(''.join((
r'(?:\s*\{\%\-|\{\%)\s*raw\s*(?:\-\%\}\s*|\%\})',
r'(?:.*)',
r'(?:.*?)',
r'(?:\s*\{\%\-|\{\%)\s*endraw\s*(?:\-\%\}\s*|\%\})',
)))
@@ -92,6 +92,18 @@ class TagIterator:
self._parenthesis_stack = []
self.pos = 0
def linepos(self, end=None) -> str:
"""Given an absolute position in the input data, return a pair of
line number + relative position to the start of the line.
"""
end_val: int = self.pos if end is None else end
data = self.data[:end_val]
# if not found, rfind returns -1, and -1+1=0, which is perfect!
last_line_start = data.rfind('\n') + 1
# it's easy to forget this, but line numbers are 1-indexed
line_number = data.count('\n') + 1
return f'{line_number}:{end_val - last_line_start}'
def advance(self, new_position):
self.pos = new_position
@@ -320,20 +332,28 @@ class BlockIterator:
dbt.exceptions.raise_compiler_error((
'Got an unexpected control flow end tag, got {} but '
'never saw a preceeding {} (@ {})'
).format(tag.block_type_name, expected, tag.start))
).format(
tag.block_type_name,
expected,
self.tag_parser.linepos(tag.start)
))
expected = _CONTROL_FLOW_TAGS[found]
if expected != tag.block_type_name:
dbt.exceptions.raise_compiler_error((
'Got an unexpected control flow end tag, got {} but '
'expected {} next (@ {})'
).format(tag.block_type_name, expected, tag.start))
).format(
tag.block_type_name,
expected,
self.tag_parser.linepos(tag.start)
))
if tag.block_type_name in allowed_blocks:
if self.stack:
dbt.exceptions.raise_compiler_error((
'Got a block definition inside control flow at {}. '
'All dbt block definitions must be at the top level'
).format(tag.start))
).format(self.tag_parser.linepos(tag.start)))
if self.current is not None:
dbt.exceptions.raise_compiler_error(
duplicate_tags.format(outer=self.current, inner=tag)

View File

@@ -4,7 +4,8 @@ import agate
import datetime
import isodate
import json
from typing import Iterable, List, Dict, Union
import dbt.utils
from typing import Iterable, List, Dict, Union, Optional, Any
from dbt.exceptions import RuntimeException
@@ -34,7 +35,11 @@ class ISODateTime(agate.data_types.DateTime):
)
def build_type_tester(text_columns: Iterable[str]):
def build_type_tester(
text_columns: Iterable[str],
string_null_values: Optional[Iterable[str]] = ('null', '')
) -> agate.TypeTester:
types = [
agate.data_types.Number(null_values=('null', '')),
agate.data_types.Date(null_values=('null', ''),
@@ -45,10 +50,10 @@ def build_type_tester(text_columns: Iterable[str]):
agate.data_types.Boolean(true_values=('true',),
false_values=('false',),
null_values=('null', '')),
agate.data_types.Text(null_values=('null', ''))
agate.data_types.Text(null_values=string_null_values)
]
force = {
k: agate.data_types.Text(null_values=('null', ''))
k: agate.data_types.Text(null_values=string_null_values)
for k in text_columns
}
return agate.TypeTester(force=force, types=types)
@@ -57,7 +62,25 @@ def build_type_tester(text_columns: Iterable[str]):
DEFAULT_TYPE_TESTER = build_type_tester(())
def table_from_data(data, column_names):
def table_from_rows(
rows: List[Any],
column_names: Iterable[str],
text_only_columns: Optional[Iterable[str]] = None,
) -> agate.Table:
if text_only_columns is None:
column_types = DEFAULT_TYPE_TESTER
else:
# If text_only_columns are present, prevent coercing empty string or
# literal 'null' strings to a None representation.
column_types = build_type_tester(
text_only_columns,
string_null_values=()
)
return agate.Table(rows, column_names, column_types=column_types)
def table_from_data(data, column_names: Iterable[str]) -> agate.Table:
"Convert list of dictionaries into an Agate table"
# The agate table is generated from a list of dicts, so the column order
@@ -72,20 +95,35 @@ def table_from_data(data, column_names):
return table.select(column_names)
def table_from_data_flat(data, column_names):
"Convert list of dictionaries into an Agate table"
def table_from_data_flat(data, column_names: Iterable[str]) -> agate.Table:
"""
Convert a list of dictionaries into an Agate table. This method does not
coerce string values into more specific types (eg. '005' will not be
coerced to '5'). Additionally, this method does not coerce values to
None (eg. '' or 'null' will retain their string literal representations).
"""
rows = []
text_only_columns = set()
for _row in data:
row = []
for value in list(_row.values()):
for col_name in column_names:
value = _row[col_name]
if isinstance(value, (dict, list, tuple)):
row.append(json.dumps(value))
else:
row.append(value)
# Represent container types as json strings
value = json.dumps(value, cls=dbt.utils.JSONEncoder)
text_only_columns.add(col_name)
elif isinstance(value, str):
text_only_columns.add(col_name)
row.append(value)
rows.append(row)
return agate.Table(rows, column_names, column_types=DEFAULT_TYPE_TESTER)
return table_from_rows(
rows=rows,
column_names=column_names,
text_only_columns=text_only_columns
)
def empty_table():

View File

@@ -4,16 +4,43 @@ import os.path
from dbt.clients.system import run_cmd, rmdir
from dbt.logger import GLOBAL_LOGGER as logger
import dbt.exceptions
from packaging import version
def clone(repo, cwd, dirname=None, remove_git_dir=False):
clone_cmd = ['git', 'clone', '--depth', '1', repo]
def _is_commit(revision: str) -> bool:
# match SHA-1 git commit
return bool(re.match(r"\b[0-9a-f]{40}\b", revision))
def clone(repo, cwd, dirname=None, remove_git_dir=False, revision=None, subdirectory=None):
has_revision = revision is not None
is_commit = _is_commit(revision or "")
clone_cmd = ['git', 'clone', '--depth', '1']
if subdirectory:
logger.debug(' Subdirectory specified: {}, using sparse checkout.'.format(subdirectory))
out, _ = run_cmd(cwd, ['git', '--version'], env={'LC_ALL': 'C'})
git_version = version.parse(re.search(r"\d+\.\d+\.\d+", out.decode("utf-8")).group(0))
if not git_version >= version.parse("2.25.0"):
# 2.25.0 introduces --sparse
raise RuntimeError(
"Please update your git version to pull a dbt package "
"from a subdirectory: your version is {}, >= 2.25.0 needed".format(git_version)
)
clone_cmd.extend(['--filter=blob:none', '--sparse'])
if has_revision and not is_commit:
clone_cmd.extend(['--branch', revision])
clone_cmd.append(repo)
if dirname is not None:
clone_cmd.append(dirname)
result = run_cmd(cwd, clone_cmd, env={'LC_ALL': 'C'})
if subdirectory:
run_cmd(os.path.join(cwd, dirname or ''), ['git', 'sparse-checkout', 'set', subdirectory])
if remove_git_dir:
rmdir(os.path.join(dirname, '.git'))
@@ -26,33 +53,38 @@ def list_tags(cwd):
return tags
def _checkout(cwd, repo, branch):
logger.debug(' Checking out branch {}.'.format(branch))
def _checkout(cwd, repo, revision):
logger.debug(' Checking out revision {}.'.format(revision))
run_cmd(cwd, ['git', 'remote', 'set-branches', 'origin', branch])
run_cmd(cwd, ['git', 'fetch', '--tags', '--depth', '1', 'origin', branch])
fetch_cmd = ["git", "fetch", "origin", "--depth", "1"]
tags = list_tags(cwd)
# Prefer tags to branches if one exists
if branch in tags:
spec = 'tags/{}'.format(branch)
if _is_commit(revision):
run_cmd(cwd, fetch_cmd + [revision])
else:
spec = 'origin/{}'.format(branch)
run_cmd(cwd, ['git', 'remote', 'set-branches', 'origin', revision])
run_cmd(cwd, fetch_cmd + ["--tags", revision])
if _is_commit(revision):
spec = revision
# Prefer tags to branches if one exists
elif revision in list_tags(cwd):
spec = 'tags/{}'.format(revision)
else:
spec = 'origin/{}'.format(revision)
out, err = run_cmd(cwd, ['git', 'reset', '--hard', spec],
env={'LC_ALL': 'C'})
return out, err
def checkout(cwd, repo, branch=None):
if branch is None:
branch = 'master'
def checkout(cwd, repo, revision=None):
if revision is None:
revision = 'HEAD'
try:
return _checkout(cwd, repo, branch)
return _checkout(cwd, repo, revision)
except dbt.exceptions.CommandResultError as exc:
stderr = exc.stderr.decode('utf-8').strip()
dbt.exceptions.bad_package_spec(repo, branch, stderr)
dbt.exceptions.bad_package_spec(repo, revision, stderr)
def get_current_sha(cwd):
@@ -66,11 +98,16 @@ def remove_remote(cwd):
def clone_and_checkout(repo, cwd, dirname=None, remove_git_dir=False,
branch=None):
revision=None, subdirectory=None):
exists = None
try:
_, err = clone(repo, cwd, dirname=dirname,
remove_git_dir=remove_git_dir)
_, err = clone(
repo,
cwd,
dirname=dirname,
remove_git_dir=remove_git_dir,
subdirectory=subdirectory,
)
except dbt.exceptions.CommandResultError as exc:
err = exc.stderr.decode('utf-8')
exists = re.match("fatal: destination path '(.+)' already exists", err)
@@ -92,7 +129,7 @@ def clone_and_checkout(repo, cwd, dirname=None, remove_git_dir=False,
logger.debug('Pulling new dependency {}.', directory)
full_path = os.path.join(cwd, directory)
start_sha = get_current_sha(full_path)
checkout(full_path, repo, branch)
checkout(full_path, repo, revision)
end_sha = get_current_sha(full_path)
if exists:
if start_sha == end_sha:
@@ -102,4 +139,4 @@ def clone_and_checkout(repo, cwd, dirname=None, remove_git_dir=False,
start_sha[:7], end_sha[:7])
else:
logger.debug(' Checked out at {}.', end_sha[:7])
return directory
return os.path.join(directory, subdirectory or '')

View File

@@ -1,29 +1,38 @@
import codecs
import linecache
import os
import re
import tempfile
import threading
from ast import literal_eval
from contextlib import contextmanager
from itertools import chain, islice
from typing import (
List, Union, Set, Optional, Dict, Any, Iterator, Type, NoReturn
List, Union, Set, Optional, Dict, Any, Iterator, Type, NoReturn, Tuple,
Callable
)
import jinja2
import jinja2.ext
import jinja2.nativetypes # type: ignore
import jinja2.nodes
import jinja2.parser
import jinja2.sandbox
from dbt.utils import (
get_dbt_macro_name, get_docs_macro_name, get_materialization_macro_name
get_dbt_macro_name, get_docs_macro_name, get_materialization_macro_name,
get_test_macro_name, deep_map
)
from dbt.clients._jinja_blocks import BlockIterator, BlockData, BlockTag
from dbt.contracts.graph.compiled import CompiledSchemaTestNode
from dbt.contracts.graph.parsed import ParsedSchemaTestNode
from dbt.exceptions import (
InternalException, raise_compiler_error, CompilationException,
invalid_materialization_argument, MacroReturn
invalid_materialization_argument, MacroReturn, JinjaRenderingException,
UndefinedMacroException
)
from dbt.flags import MACRO_DEBUGGING
from dbt import flags
from dbt.logger import GLOBAL_LOGGER as logger # noqa
@@ -86,31 +95,119 @@ class MacroFuzzEnvironment(jinja2.sandbox.SandboxedEnvironment):
If the value is 'write', also write the files to disk.
WARNING: This can write a ton of data if you aren't careful.
"""
if filename == '<template>' and MACRO_DEBUGGING:
write = MACRO_DEBUGGING == 'write'
if filename == '<template>' and flags.MACRO_DEBUGGING:
write = flags.MACRO_DEBUGGING == 'write'
filename = _linecache_inject(source, write)
return super()._compile(source, filename) # type: ignore
class NativeSandboxEnvironment(MacroFuzzEnvironment):
code_generator_class = jinja2.nativetypes.NativeCodeGenerator
class TextMarker(str):
"""A special native-env marker that indicates that a value is text and is
not to be evaluated. Use this to prevent your numbery-strings from becoming
numbers!
"""
class NativeMarker(str):
"""A special native-env marker that indicates the field should be passed to
literal_eval.
"""
class BoolMarker(NativeMarker):
pass
class NumberMarker(NativeMarker):
pass
def _is_number(value) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool)
def quoted_native_concat(nodes):
"""This is almost native_concat from the NativeTemplate, except in the
special case of a single argument that is a quoted string and returns a
string, the quotes are re-inserted.
"""
head = list(islice(nodes, 2))
if not head:
return ''
if len(head) == 1:
raw = head[0]
if isinstance(raw, TextMarker):
return str(raw)
elif not isinstance(raw, NativeMarker):
# return non-strings as-is
return raw
else:
# multiple nodes become a string.
return "".join([str(v) for v in chain(head, nodes)])
try:
result = literal_eval(raw)
except (ValueError, SyntaxError, MemoryError):
result = raw
if isinstance(raw, BoolMarker) and not isinstance(result, bool):
raise JinjaRenderingException(
f"Could not convert value '{raw!s}' into type 'bool'"
)
if isinstance(raw, NumberMarker) and not _is_number(result):
raise JinjaRenderingException(
f"Could not convert value '{raw!s}' into type 'number'"
)
return result
class NativeSandboxTemplate(jinja2.nativetypes.NativeTemplate): # mypy: ignore
environment_class = NativeSandboxEnvironment
def render(self, *args, **kwargs):
"""Render the template to produce a native Python type. If the
result is a single node, its value is returned. Otherwise, the
nodes are concatenated as strings. If the result can be parsed
with :func:`ast.literal_eval`, the parsed value is returned.
Otherwise, the string is returned.
"""
vars = dict(*args, **kwargs)
try:
return quoted_native_concat(
self.root_render_func(self.new_context(vars))
)
except Exception:
return self.environment.handle_exception()
NativeSandboxEnvironment.template_class = NativeSandboxTemplate # type: ignore
class TemplateCache:
def __init__(self):
self.file_cache = {}
self.file_cache: Dict[str, jinja2.Template] = {}
def get_node_template(self, node):
key = (node.package_name, node.original_file_path)
def get_node_template(self, node) -> jinja2.Template:
key = node.macro_sql
if key in self.file_cache:
return self.file_cache[key]
template = get_template(
string=node.raw_sql,
string=node.macro_sql,
ctx={},
node=node,
)
self.file_cache[key] = template
self.file_cache[key] = template
return template
def clear(self):
@@ -135,6 +232,7 @@ class BaseMacroGenerator:
template = self.get_template()
# make the module. previously we set both vars and local, but that's
# redundant: They both end up in the same place
# make_module is in jinja2.environment. It returns a TemplateModule
module = template.make_module(vars=self.context, shared=False)
macro = module.__dict__[get_dbt_macro_name(name)]
module.__dict__.update(self.context)
@@ -148,6 +246,7 @@ class BaseMacroGenerator:
raise_compiler_error(str(e))
def call_macro(self, *args, **kwargs):
# called from __call__ methods
if self.context is None:
raise InternalException(
'Context is still None in call_macro!'
@@ -210,8 +309,10 @@ class MacroGenerator(BaseMacroGenerator):
e.stack.append(self.macro)
raise e
# This adds the macro's unique id to the node's 'depends_on'
@contextmanager
def track_call(self):
# This is only called from __call__
if self.stack is None or self.node is None:
yield
else:
@@ -226,6 +327,7 @@ class MacroGenerator(BaseMacroGenerator):
finally:
self.stack.pop(unique_id)
# this makes MacroGenerator objects callable like functions
def __call__(self, *args, **kwargs):
with self.track_call():
return self.call_macro(*args, **kwargs)
@@ -307,42 +409,36 @@ class DocumentationExtension(jinja2.ext.Extension):
return node
class TestExtension(jinja2.ext.Extension):
tags = ['test']
def parse(self, parser):
node = jinja2.nodes.Macro(lineno=next(parser.stream).lineno)
test_name = parser.parse_assign_target(name_only=True).name
parser.parse_signature(node)
node.name = get_test_macro_name(test_name)
node.body = parser.parse_statements(('name:endtest',),
drop_needle=True)
return node
def _is_dunder_name(name):
return name.startswith('__') and name.endswith('__')
def create_macro_capture_env(node):
class ParserMacroCapture(jinja2.Undefined):
"""
This class sets up the parser to capture macros.
"""
def create_undefined(node=None):
class Undefined(jinja2.Undefined):
def __init__(self, hint=None, obj=None, name=None, exc=None):
super().__init__(hint=hint, name=name)
self.node = node
self.name = name
self.package_name = node.package_name
self.hint = hint
# jinja uses these for safety, so we have to override them.
# see https://github.com/pallets/jinja/blob/master/jinja2/sandbox.py#L332-L339 # noqa
self.unsafe_callable = False
self.alters_data = False
def __deepcopy__(self, memo):
path = os.path.join(self.node.root_path,
self.node.original_file_path)
logger.debug(
'dbt encountered an undefined variable, "{}" in node {}.{} '
'(source path: {})'
.format(self.name, self.node.package_name,
self.node.name, path))
# match jinja's message
raise_compiler_error(
"{!r} is undefined".format(self.name),
node=self.node
)
def __getitem__(self, name):
# Propagate the undefined value if a caller accesses this as if it
# were a dictionary
@@ -355,29 +451,64 @@ def create_macro_capture_env(node):
.format(type(self).__name__, name)
)
self.package_name = self.name
self.name = name
return self
return self.__class__(hint=self.hint, name=self.name)
def __call__(self, *args, **kwargs):
return self
return ParserMacroCapture
def __reduce__(self):
raise_compiler_error(f'{self.name} is undefined', node=node)
return Undefined
def get_environment(node=None, capture_macros=False):
NATIVE_FILTERS: Dict[str, Callable[[Any], Any]] = {
'as_text': TextMarker,
'as_bool': BoolMarker,
'as_native': NativeMarker,
'as_number': NumberMarker,
}
TEXT_FILTERS: Dict[str, Callable[[Any], Any]] = {
'as_text': lambda x: x,
'as_bool': lambda x: x,
'as_native': lambda x: x,
'as_number': lambda x: x,
}
def get_environment(
node=None,
capture_macros: bool = False,
native: bool = False,
) -> jinja2.Environment:
args: Dict[str, List[Union[str, Type[jinja2.ext.Extension]]]] = {
'extensions': ['jinja2.ext.do']
}
if capture_macros:
args['undefined'] = create_macro_capture_env(node)
args['undefined'] = create_undefined(node)
args['extensions'].append(MaterializationExtension)
args['extensions'].append(DocumentationExtension)
args['extensions'].append(TestExtension)
return MacroFuzzEnvironment(**args)
env_cls: Type[jinja2.Environment]
text_filter: Type
if native:
env_cls = NativeSandboxEnvironment
filters = NATIVE_FILTERS
else:
env_cls = MacroFuzzEnvironment
filters = TEXT_FILTERS
env = env_cls(**args)
env.filters.update(filters)
return env
@contextmanager
@@ -388,7 +519,10 @@ def catch_jinja(node=None) -> Iterator[None]:
e.translated = False
raise CompilationException(str(e), node) from e
except jinja2.exceptions.UndefinedError as e:
raise CompilationException(str(e), node) from e
raise UndefinedMacroException(str(e), node) from e
except CompilationException as exc:
exc.add_node(node)
raise
def parse(string):
@@ -396,22 +530,71 @@ def parse(string):
return get_environment().parse(str(string))
def get_template(string, ctx, node=None, capture_macros=False):
def get_template(
string: str,
ctx: Dict[str, Any],
node=None,
capture_macros: bool = False,
native: bool = False,
):
with catch_jinja(node):
env = get_environment(node, capture_macros)
env = get_environment(node, capture_macros, native=native)
template_source = str(string)
return env.from_string(template_source, globals=ctx)
def render_template(template, ctx, node=None):
def render_template(template, ctx: Dict[str, Any], node=None) -> str:
with catch_jinja(node):
return template.render(ctx)
def get_rendered(string, ctx, node=None, capture_macros=False):
template = get_template(string, ctx, node, capture_macros=capture_macros)
def _requote_result(raw_value: str, rendered: str) -> str:
double_quoted = raw_value.startswith('"') and raw_value.endswith('"')
single_quoted = raw_value.startswith("'") and raw_value.endswith("'")
if double_quoted:
quote_char = '"'
elif single_quoted:
quote_char = "'"
else:
quote_char = ''
return f'{quote_char}{rendered}{quote_char}'
# performance note: Local benmcharking (so take it with a big grain of salt!)
# on this indicates that it is is on average slightly slower than
# checking two separate patterns, but the standard deviation is smaller with
# one pattern. The time difference between the two was ~2 std deviations, which
# is small enough that I've just chosen the more readable option.
_HAS_RENDER_CHARS_PAT = re.compile(r'({[{%#]|[#}%]})')
def get_rendered(
string: str,
ctx: Dict[str, Any],
node=None,
capture_macros: bool = False,
native: bool = False,
) -> str:
# performance optimization: if there are no jinja control characters in the
# string, we can just return the input. Fall back to jinja if the type is
# not a string or if native rendering is enabled (so '1' -> 1, etc...)
# If this is desirable in the native env as well, we could handle the
# native=True case by passing the input string to ast.literal_eval, like
# the native renderer does.
if (
not native and
isinstance(string, str) and
_HAS_RENDER_CHARS_PAT.search(string) is None
):
return string
template = get_template(
string,
ctx,
node,
capture_macros=capture_macros,
native=native,
)
return render_template(template, ctx, node)
@@ -442,3 +625,41 @@ def extract_toplevel_blocks(
allowed_blocks=allowed_blocks,
collect_raw_data=collect_raw_data
)
SCHEMA_TEST_KWARGS_NAME = '_dbt_schema_test_kwargs'
def add_rendered_test_kwargs(
context: Dict[str, Any],
node: Union[ParsedSchemaTestNode, CompiledSchemaTestNode],
capture_macros: bool = False,
) -> None:
"""Render each of the test kwargs in the given context using the native
renderer, then insert that value into the given context as the special test
keyword arguments member.
"""
looks_like_func = r'^\s*(env_var|ref|var|source|doc)\s*\(.+\)\s*$'
def _convert_function(
value: Any, keypath: Tuple[Union[str, int], ...]
) -> Any:
if isinstance(value, str):
if keypath == ('column_name',):
# special case: Don't render column names as native, make them
# be strings
return value
if re.match(looks_like_func, value) is not None:
# curly braces to make rendering happy
value = f'{{{{ {value} }}}}'
value = get_rendered(
value, context, node, capture_macros=capture_macros,
native=True
)
return value
kwargs = deep_map(_convert_function, node.test_metadata.kwargs)
context[SCHEMA_TEST_KWARGS_NAME] = kwargs

View File

@@ -0,0 +1,225 @@
import jinja2
from dbt.clients.jinja import get_environment
from dbt.exceptions import raise_compiler_error
def statically_extract_macro_calls(string, ctx, db_wrapper=None):
# set 'capture_macros' to capture undefined
env = get_environment(None, capture_macros=True)
parsed = env.parse(string)
standard_calls = ['source', 'ref', 'config']
possible_macro_calls = []
for func_call in parsed.find_all(jinja2.nodes.Call):
func_name = None
if hasattr(func_call, 'node') and hasattr(func_call.node, 'name'):
func_name = func_call.node.name
else:
# func_call for dbt_utils.current_timestamp macro
# Call(
# node=Getattr(
# node=Name(
# name='dbt_utils',
# ctx='load'
# ),
# attr='current_timestamp',
# ctx='load
# ),
# args=[],
# kwargs=[],
# dyn_args=None,
# dyn_kwargs=None
# )
if (hasattr(func_call, 'node') and
hasattr(func_call.node, 'node') and
type(func_call.node.node).__name__ == 'Name' and
hasattr(func_call.node, 'attr')):
package_name = func_call.node.node.name
macro_name = func_call.node.attr
if package_name == 'adapter':
if macro_name == 'dispatch':
ad_macro_calls = statically_parse_adapter_dispatch(
func_call, ctx, db_wrapper)
possible_macro_calls.extend(ad_macro_calls)
else:
# This skips calls such as adapter.parse_index
continue
else:
func_name = f'{package_name}.{macro_name}'
else:
continue
if not func_name:
continue
if func_name in standard_calls:
continue
elif ctx.get(func_name):
continue
else:
if func_name not in possible_macro_calls:
possible_macro_calls.append(func_name)
return possible_macro_calls
# Call(
# node=Getattr(
# node=Name(
# name='adapter',
# ctx='load'
# ),
# attr='dispatch',
# ctx='load'
# ),
# args=[
# Const(value='test_pkg_and_dispatch')
# ],
# kwargs=[
# Keyword(
# key='packages',
# value=Call(node=Getattr(node=Name(name='local_utils', ctx='load'),
# attr='_get_utils_namespaces', ctx='load'), args=[], kwargs=[],
# dyn_args=None, dyn_kwargs=None)
# )
# ],
# dyn_args=None,
# dyn_kwargs=None
# )
def statically_parse_adapter_dispatch(func_call, ctx, db_wrapper):
possible_macro_calls = []
# This captures an adapter.dispatch('<macro_name>') call.
func_name = None
# macro_name positional argument
if len(func_call.args) > 0:
func_name = func_call.args[0].value
if func_name:
possible_macro_calls.append(func_name)
# packages positional argument
packages = None
macro_namespace = None
packages_arg = None
packages_arg_type = None
if len(func_call.args) > 1:
packages_arg = func_call.args[1]
# This can be a List or a Call
packages_arg_type = type(func_call.args[1]).__name__
# keyword arguments
if func_call.kwargs:
for kwarg in func_call.kwargs:
if kwarg.key == 'packages':
# The packages keyword will be deprecated and
# eventually removed
packages_arg = kwarg.value
# This can be a List or a Call
packages_arg_type = type(kwarg.value).__name__
elif kwarg.key == 'macro_name':
# This will remain to enable static resolution
if type(kwarg.value).__name__ == 'Const':
func_name = kwarg.value.value
possible_macro_calls.append(func_name)
else:
raise_compiler_error(f"The macro_name parameter ({kwarg.value.value}) "
"to adapter.dispatch was not a string")
elif kwarg.key == 'macro_namespace':
# This will remain to enable static resolution
kwarg_type = type(kwarg.value).__name__
if kwarg_type == 'Const':
macro_namespace = kwarg.value.value
else:
raise_compiler_error("The macro_namespace parameter to adapter.dispatch "
f"is a {kwarg_type}, not a string")
# positional arguments
if packages_arg:
if packages_arg_type == 'List':
# This will remain to enable static resolution
packages = []
for item in packages_arg.items:
packages.append(item.value)
elif packages_arg_type == 'Const':
# This will remain to enable static resolution
macro_namespace = packages_arg.value
elif packages_arg_type == 'Call':
# This is deprecated and should be removed eventually.
# It is here to support (hackily) common ways of providing
# a packages list to adapter.dispatch
if (hasattr(packages_arg, 'node') and
hasattr(packages_arg.node, 'node') and
hasattr(packages_arg.node.node, 'name') and
hasattr(packages_arg.node, 'attr')):
package_name = packages_arg.node.node.name
macro_name = packages_arg.node.attr
if (macro_name.startswith('_get') and 'namespaces' in macro_name):
# noqa: https://github.com/dbt-labs/dbt-utils/blob/9e9407b/macros/cross_db_utils/_get_utils_namespaces.sql
var_name = f'{package_name}_dispatch_list'
# hard code compatibility for fivetran_utils, just a teensy bit different
# noqa: https://github.com/fivetran/dbt_fivetran_utils/blob/0978ba2/macros/_get_utils_namespaces.sql
if package_name == 'fivetran_utils':
default_packages = ['dbt_utils', 'fivetran_utils']
else:
default_packages = [package_name]
namespace_names = get_dispatch_list(ctx, var_name, default_packages)
packages = []
if namespace_names:
packages.extend(namespace_names)
else:
msg = (
f"As of v0.19.2, custom macros, such as '{macro_name}', are no longer "
"supported in the 'packages' argument of 'adapter.dispatch()'.\n"
f"See https://docs.getdbt.com/reference/dbt-jinja-functions/dispatch "
"for details."
).strip()
raise_compiler_error(msg)
elif packages_arg_type == 'Add':
# This logic is for when there is a variable and an addition of a list,
# like: packages = (var('local_utils_dispatch_list', []) + ['local_utils2'])
# This is deprecated and should be removed eventually.
namespace_var = None
default_namespaces = []
# This might be a single call or it might be the 'left' piece in an addition
for var_call in packages_arg.find_all(jinja2.nodes.Call):
if (hasattr(var_call, 'node') and
var_call.node.name == 'var' and
hasattr(var_call, 'args')):
namespace_var = var_call.args[0].value
if hasattr(packages_arg, 'right'): # we have a default list of namespaces
for item in packages_arg.right.items:
default_namespaces.append(item.value)
if namespace_var:
namespace_names = get_dispatch_list(ctx, namespace_var, default_namespaces)
packages = []
if namespace_names:
packages.extend(namespace_names)
if db_wrapper:
macro = db_wrapper.dispatch(
func_name,
packages=packages,
macro_namespace=macro_namespace
).macro
func_name = f'{macro.package_name}.{macro.name}'
possible_macro_calls.append(func_name)
else: # this is only for test/unit/test_macro_calls.py
if macro_namespace:
packages = [macro_namespace]
if packages is None:
packages = []
for package_name in packages:
possible_macro_calls.append(f'{package_name}.{func_name}')
return possible_macro_calls
def get_dispatch_list(ctx, var_name, default_packages):
namespace_list = None
try:
# match the logic currently used in package _get_namespaces() macro
namespace_list = ctx['var'](var_name) + default_packages
except Exception:
pass
namespace_list = namespace_list if namespace_list else default_packages
return namespace_list

View File

@@ -1,10 +1,9 @@
from functools import wraps
import functools
import requests
from dbt.exceptions import RegistryException
from dbt.utils import memoized
from dbt.utils import memoized, _connection_exception_retry as connection_exception_retry
from dbt.logger import GLOBAL_LOGGER as logger
from dbt import deprecations
import os
import time
if os.getenv('DBT_PACKAGE_HUB_URL'):
DEFAULT_REGISTRY_BASE_URL = os.getenv('DBT_PACKAGE_HUB_URL')
@@ -19,31 +18,15 @@ def _get_url(url, registry_base_url=None):
return '{}{}'.format(registry_base_url, url)
def _wrap_exceptions(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
max_attempts = 5
attempt = 0
while True:
attempt += 1
try:
return fn(*args, **kwargs)
except requests.exceptions.ConnectionError as exc:
if attempt < max_attempts:
time.sleep(1)
continue
raise RegistryException(
'Unable to connect to registry hub'
) from exc
return wrapper
def _get_with_retries(path, registry_base_url=None):
get_fn = functools.partial(_get, path, registry_base_url)
return connection_exception_retry(get_fn, 5)
@_wrap_exceptions
def _get(path, registry_base_url=None):
url = _get_url(path, registry_base_url)
logger.debug('Making package registry request: GET {}'.format(url))
resp = requests.get(url)
resp = requests.get(url, timeout=30)
logger.debug('Response from registry: GET {} {}'.format(url,
resp.status_code))
resp.raise_for_status()
@@ -51,22 +34,44 @@ def _get(path, registry_base_url=None):
def index(registry_base_url=None):
return _get('api/v1/index.json', registry_base_url)
return _get_with_retries('api/v1/index.json', registry_base_url)
index_cached = memoized(index)
def packages(registry_base_url=None):
return _get('api/v1/packages.json', registry_base_url)
return _get_with_retries('api/v1/packages.json', registry_base_url)
def package(name, registry_base_url=None):
return _get('api/v1/{}.json'.format(name), registry_base_url)
response = _get_with_retries('api/v1/{}.json'.format(name), registry_base_url)
# Either redirectnamespace or redirectname in the JSON response indicate a redirect
# redirectnamespace redirects based on package ownership
# redirectname redirects based on package name
# Both can be present at the same time, or neither. Fails gracefully to old name
if ('redirectnamespace' in response) or ('redirectname' in response):
if ('redirectnamespace' in response) and response['redirectnamespace'] is not None:
use_namespace = response['redirectnamespace']
else:
use_namespace = response['namespace']
if ('redirectname' in response) and response['redirectname'] is not None:
use_name = response['redirectname']
else:
use_name = response['name']
new_nwo = use_namespace + "/" + use_name
deprecations.warn('package-redirect', old_name=name, new_name=new_nwo)
return response
def package_version(name, version, registry_base_url=None):
return _get('api/v1/{}/{}.json'.format(name, version), registry_base_url)
return _get_with_retries('api/v1/{}/{}.json'.format(name, version), registry_base_url)
def get_available_versions(name):

View File

@@ -1,8 +1,10 @@
import errno
import functools
import fnmatch
import json
import os
import os.path
import re
import shutil
import subprocess
import sys
@@ -10,13 +12,18 @@ import tarfile
import requests
import stat
from typing import (
Type, NoReturn, List, Optional, Dict, Any, Tuple, Callable
Type, NoReturn, List, Optional, Dict, Any, Tuple, Callable, Union
)
import dbt.exceptions
import dbt.utils
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.utils import _connection_exception_retry as connection_exception_retry
if sys.platform == 'win32':
from ctypes import WinDLL, c_bool
else:
WinDLL = None
c_bool = None
def find_matching(
@@ -40,6 +47,8 @@ def find_matching(
"""
matching = []
root_path = os.path.normpath(root_path)
regex = fnmatch.translate(file_pattern)
reobj = re.compile(regex, re.IGNORECASE)
for relative_path_to_search in relative_paths_to_search:
absolute_path_to_search = os.path.join(
@@ -52,8 +61,7 @@ def find_matching(
relative_path = os.path.relpath(
absolute_path, absolute_path_to_search
)
if fnmatch.fnmatch(local_file, file_pattern):
if reobj.match(local_file):
matching.append({
'searched_path': relative_path_to_search,
'absolute_path': absolute_path,
@@ -64,6 +72,7 @@ def find_matching(
def load_file_contents(path: str, strip: bool = True) -> str:
path = convert_path(path)
with open(path, 'rb') as handle:
to_return = handle.read().decode('utf-8')
@@ -79,6 +88,7 @@ def make_directory(path: str) -> None:
exist. This function handles the case where two threads try to create
a directory at once.
"""
path = convert_path(path)
if not os.path.exists(path):
# concurrent writes that try to create the same dir can fail
try:
@@ -97,6 +107,7 @@ def make_file(path: str, contents: str = '', overwrite: bool = False) -> bool:
exists. The file is saved with contents `contents`
"""
if overwrite or not os.path.exists(path):
path = convert_path(path)
with open(path, 'w') as fh:
fh.write(contents)
return True
@@ -119,13 +130,42 @@ def supports_symlinks() -> bool:
def write_file(path: str, contents: str = '') -> bool:
make_directory(os.path.dirname(path))
with open(path, 'w', encoding='utf-8') as f:
f.write(str(contents))
path = convert_path(path)
try:
make_directory(os.path.dirname(path))
with open(path, 'w', encoding='utf-8') as f:
f.write(str(contents))
except Exception as exc:
# note that you can't just catch FileNotFound, because sometimes
# windows apparently raises something else.
# It's also not sufficient to look at the path length, because
# sometimes windows fails to write paths that are less than the length
# limit. So on windows, suppress all errors that happen from writing
# to disk.
if os.name == 'nt':
# sometimes we get a winerror of 3 which means the path was
# definitely too long, but other times we don't and it means the
# path was just probably too long. This is probably based on the
# windows/python version.
if getattr(exc, 'winerror', 0) == 3:
reason = 'Path was too long'
else:
reason = 'Path was possibly too long'
# all our hard work and the path was still too long. Log and
# continue.
logger.debug(
f'Could not write to path {path}({len(path)} characters): '
f'{reason}\nexception: {exc}'
)
else:
raise
return True
def read_json(path: str) -> Dict[str, Any]:
return json.loads(load_file_contents(path))
def write_json(path: str, data: Dict[str, Any]) -> bool:
return write_file(path, json.dumps(data, cls=dbt.utils.JSONEncoder))
@@ -161,7 +201,7 @@ def rmdir(path: str) -> None:
different permissions on Windows. Otherwise, removing directories (eg.
cloned via git) can cause rmtree to throw a PermissionError exception
"""
logger.debug("DEBUG** Window rmdir sys.platform: {}".format(sys.platform))
path = convert_path(path)
if sys.platform == 'win32':
onerror = _windows_rmdir_readonly
else:
@@ -170,15 +210,90 @@ def rmdir(path: str) -> None:
shutil.rmtree(path, onerror=onerror)
def _win_prepare_path(path: str) -> str:
"""Given a windows path, prepare it for use by making sure it is absolute
and normalized.
"""
path = os.path.normpath(path)
# if a path starts with '\', splitdrive() on it will return '' for the
# drive, but the prefix requires a drive letter. So let's add the drive
# letter back in.
# Unless it starts with '\\'. In that case, the path is a UNC mount point
# and splitdrive will be fine.
if not path.startswith('\\\\') and path.startswith('\\'):
curdrive = os.path.splitdrive(os.getcwd())[0]
path = curdrive + path
# now our path is either an absolute UNC path or relative to the current
# directory. If it's relative, we need to make it absolute or the prefix
# won't work. `ntpath.abspath` allegedly doesn't always play nice with long
# paths, so do this instead.
if not os.path.splitdrive(path)[0]:
path = os.path.join(os.getcwd(), path)
return path
def _supports_long_paths() -> bool:
if sys.platform != 'win32':
return True
# Eryk Sun says to use `WinDLL('ntdll')` instead of `windll.ntdll` because
# of pointer caching in a comment here:
# https://stackoverflow.com/a/35097999/11262881
# I don't know exaclty what he means, but I am inclined to believe him as
# he's pretty active on Python windows bugs!
try:
dll = WinDLL('ntdll')
except OSError: # I don't think this happens? you need ntdll to run python
return False
# not all windows versions have it at all
if not hasattr(dll, 'RtlAreLongPathsEnabled'):
return False
# tell windows we want to get back a single unsigned byte (a bool).
dll.RtlAreLongPathsEnabled.restype = c_bool
return dll.RtlAreLongPathsEnabled()
def convert_path(path: str) -> str:
"""Convert a path that dbt has, which might be >260 characters long, to one
that will be writable/readable on Windows.
On other platforms, this is a no-op.
"""
# some parts of python seem to append '\*.*' to strings, better safe than
# sorry.
if len(path) < 250:
return path
if _supports_long_paths():
return path
prefix = '\\\\?\\'
# Nothing to do
if path.startswith(prefix):
return path
path = _win_prepare_path(path)
# add the prefix. The check is just in case os.getcwd() does something
# unexpected - I believe this if-state should always be True though!
if not path.startswith(prefix):
path = prefix + path
return path
def remove_file(path: str) -> None:
path = convert_path(path)
os.remove(path)
def path_exists(path: str) -> bool:
path = convert_path(path)
return os.path.lexists(path)
def path_is_symlink(path: str) -> bool:
path = convert_path(path)
return os.path.islink(path)
@@ -301,6 +416,9 @@ def run_cmd(
full_env.update(env)
try:
exe_pth = shutil.which(cmd[0])
if exe_pth:
cmd = [os.path.abspath(exe_pth)] + list(cmd[1:])
proc = subprocess.Popen(
cmd,
cwd=cwd,
@@ -323,14 +441,27 @@ def run_cmd(
return out, err
def download(url: str, path: str) -> None:
response = requests.get(url)
def download_with_retries(
url: str, path: str, timeout: Optional[Union[float, tuple]] = None
) -> None:
download_fn = functools.partial(download, url, path, timeout)
connection_exception_retry(download_fn, 5)
def download(
url: str, path: str, timeout: Optional[Union[float, tuple]] = None
) -> None:
path = convert_path(path)
connection_timeout = timeout or float(os.getenv('DBT_HTTP_TIMEOUT', 10))
response = requests.get(url, timeout=connection_timeout)
with open(path, 'wb') as handle:
for block in response.iter_content(1024 * 64):
handle.write(block)
def rename(from_path: str, to_path: str, force: bool = False) -> None:
from_path = convert_path(from_path)
to_path = convert_path(to_path)
is_symlink = path_is_symlink(to_path)
if os.path.exists(to_path) and force:
@@ -345,6 +476,7 @@ def rename(from_path: str, to_path: str, force: bool = False) -> None:
def untar_package(
tar_path: str, dest_dir: str, rename_to: Optional[str] = None
) -> None:
tar_path = convert_path(tar_path)
tar_dir_name = None
with tarfile.open(tar_path, 'r') as tarball:
tarball.extractall(dest_dir)
@@ -381,6 +513,8 @@ def move(src, dst):
This is almost identical to the real shutil.move, except it uses our rmtree
and skips handling non-windows OSes since the existing one works ok there.
"""
src = convert_path(src)
dst = convert_path(dst)
if os.name != 'nt':
return shutil.move(src, dst)
@@ -415,4 +549,5 @@ def rmtree(path):
"""Recursively remove path. On permissions errors on windows, try to remove
the read-only flag and try again.
"""
path = convert_path(path)
return shutil.rmtree(path, onerror=chmod_and_retry)

View File

@@ -1,8 +1,20 @@
import dbt.exceptions
from typing import Any, Dict, Optional
import yaml
import yaml.scanner
# the C version is faster, but it doesn't always exist
try:
from yaml import (
CLoader as Loader,
CSafeLoader as SafeLoader,
CDumper as Dumper
)
except ImportError:
from yaml import ( # type: ignore # noqa: F401
Loader, SafeLoader, Dumper
)
YAML_ERROR_MESSAGE = """
Syntax error near line {line_number}
@@ -44,9 +56,13 @@ def contextualized_yaml_error(raw_contents, error):
raw_error=error)
def safe_load(contents) -> Optional[Dict[str, Any]]:
return yaml.load(contents, Loader=SafeLoader)
def load_yaml_text(contents):
try:
return yaml.safe_load(contents)
return safe_load(contents)
except (yaml.scanner.ScannerError, yaml.YAMLError) as e:
if hasattr(e, 'problem_mark'):
error = contextualized_yaml_error(contents, e)

View File

@@ -1,35 +1,45 @@
import itertools
import os
from collections import defaultdict
from typing import List, Dict
from typing import List, Dict, Any, Tuple, cast, Optional
import dbt.utils
import dbt.include
import dbt.tracking
from dbt.node_types import NodeType
from dbt.linker import Linker
import networkx as nx # type: ignore
import sqlparse
from dbt import flags
from dbt.adapters.factory import get_adapter
from dbt.clients import jinja
from dbt.clients.system import make_directory
from dbt.context.providers import generate_runtime_model
import dbt.contracts.project
import dbt.exceptions
import dbt.flags
import dbt.config
from dbt.contracts.graph.compiled import InjectedCTE, COMPILED_TYPES
from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.graph.compiled import (
COMPILED_TYPES,
CompiledSchemaTestNode,
GraphMemberNode,
InjectedCTE,
ManifestNode,
NonSourceCompiledNode,
)
from dbt.contracts.graph.parsed import ParsedNode
from dbt.exceptions import (
dependency_not_found,
InternalException,
RuntimeException,
)
from dbt.graph import Graph
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.node_types import NodeType
from dbt.utils import pluralize
import dbt.tracking
graph_file_name = 'graph.gpickle'
def _compiled_type_for(model: ParsedNode):
if model.resource_type not in COMPILED_TYPES:
raise dbt.exceptions.InternalException(
'Asked to compile {} node, but it has no compiled form'
.format(model.resource_type)
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[model.resource_type]
return COMPILED_TYPES[type(model)]
def print_compile_stats(stats):
@@ -42,20 +52,26 @@ def print_compile_stats(stats):
NodeType.Operation: 'operation',
NodeType.Seed: 'seed file',
NodeType.Source: 'source',
NodeType.Exposure: 'exposure',
}
results = {k: 0 for k in names.keys()}
results.update(stats)
# create tracking event for resource_counts
if dbt.tracking.active_user is not None:
resource_counts = {k.pluralize(): v for k, v in results.items()}
dbt.tracking.track_resource_counts(resource_counts)
stat_line = ", ".join([
dbt.utils.pluralize(ct, names.get(t)) for t, ct in results.items()
pluralize(ct, names.get(t)) for t, ct in results.items()
if t in names
])
logger.info("Found {}".format(stat_line))
def _node_enabled(node):
def _node_enabled(node: ManifestNode):
# Disabled models are already excluded from the manifest
if node.resource_type == NodeType.Test and not node.config.enabled:
return False
@@ -63,14 +79,18 @@ def _node_enabled(node):
return True
def _generate_stats(manifest):
def _generate_stats(manifest: Manifest):
stats: Dict[NodeType, int] = defaultdict(int)
for node_name, node in itertools.chain(
manifest.nodes.items(),
manifest.macros.items()):
for node in manifest.nodes.values():
if _node_enabled(node):
stats[node.resource_type] += 1
for source in manifest.sources.values():
stats[source.resource_type] += 1
for exposure in manifest.exposures.values():
stats[exposure.resource_type] += 1
for macro in manifest.macros.values():
stats[macro.resource_type] += 1
return stats
@@ -87,39 +107,45 @@ def _extend_prepended_ctes(prepended_ctes, new_prepended_ctes):
_add_prepended_cte(prepended_ctes, new_cte)
def prepend_ctes(model, manifest):
model, _, manifest = recursively_prepend_ctes(model, manifest)
class Linker:
def __init__(self, data=None):
if data is None:
data = {}
self.graph = nx.DiGraph(**data)
return (model, manifest)
def edges(self):
return self.graph.edges()
def nodes(self):
return self.graph.nodes()
def recursively_prepend_ctes(model, manifest):
if model.extra_ctes_injected:
return (model, model.extra_ctes, manifest)
def find_cycles(self):
try:
cycle = nx.find_cycle(self.graph)
except nx.NetworkXNoCycle:
return None
else:
# cycles is a List[Tuple[str, ...]]
return " --> ".join(c[0] for c in cycle)
if dbt.flags.STRICT_MODE:
if not isinstance(model, tuple(COMPILED_TYPES.values())):
raise dbt.exceptions.InternalException(
'Bad model type: {}'.format(type(model))
)
def dependency(self, node1, node2):
"indicate that node1 depends on node2"
self.graph.add_node(node1)
self.graph.add_node(node2)
self.graph.add_edge(node2, node1)
prepended_ctes: List[InjectedCTE] = []
def add_node(self, node):
self.graph.add_node(node)
for cte in model.extra_ctes:
cte_id = cte.id
cte_to_add = manifest.nodes.get(cte_id)
cte_to_add, new_prepended_ctes, manifest = recursively_prepend_ctes(
cte_to_add, manifest)
_extend_prepended_ctes(prepended_ctes, new_prepended_ctes)
new_cte_name = '__dbt__CTE__{}'.format(cte_to_add.name)
sql = ' {} as (\n{}\n)'.format(new_cte_name, cte_to_add.compiled_sql)
_add_prepended_cte(prepended_ctes, InjectedCTE(id=cte_id, sql=sql))
model.prepend_ctes(prepended_ctes)
manifest.update_node(model)
return (model, prepended_ctes, manifest)
def write_graph(self, outfile: str, manifest: Manifest):
"""Write the graph to a gpickle file. Before doing so, serialize and
include all nodes in their corresponding graph entries.
"""
out_graph = self.graph.copy()
for node_id in self.graph.nodes():
data = manifest.expect(node_id).to_dict(omit_none=True)
out_graph.add_node(node_id, **data)
nx.write_gpickle(out_graph, outfile)
class Compiler:
@@ -127,95 +153,274 @@ class Compiler:
self.config = config
def initialize(self):
dbt.clients.system.make_directory(self.config.target_path)
dbt.clients.system.make_directory(self.config.modules_path)
make_directory(self.config.target_path)
make_directory(self.config.modules_path)
def compile_node(self, node, manifest, extra_context=None):
# creates a ModelContext which is converted to
# a dict for jinja rendering of SQL
def _create_node_context(
self,
node: NonSourceCompiledNode,
manifest: Manifest,
extra_context: Dict[str, Any],
) -> Dict[str, Any]:
context = generate_runtime_model(
node, self.config, manifest
)
context.update(extra_context)
if isinstance(node, CompiledSchemaTestNode):
# for test nodes, add a special keyword args value to the context
jinja.add_rendered_test_kwargs(context, node)
return context
def add_ephemeral_prefix(self, name: str):
adapter = get_adapter(self.config)
relation_cls = adapter.Relation
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:
"""
`ctes` is a list of InjectedCTEs like:
[
InjectedCTE(
id="cte_id_1",
sql="__dbt__cte__ephemeral as (select * from table)",
),
InjectedCTE(
id="cte_id_2",
sql="__dbt__cte__events as (select id, type from events)",
),
]
Given `sql` like:
"with internal_cte as (select * from sessions)
select * from internal_cte"
This will spit out:
"with __dbt__cte__ephemeral as (select * from table),
__dbt__cte__events as (select id, type from events),
with internal_cte as (select * from sessions)
select * from internal_cte"
(Whitespace enhanced for readability.)
"""
if len(ctes) == 0:
return sql
parsed_stmts = sqlparse.parse(sql)
parsed = parsed_stmts[0]
with_stmt = None
for token in parsed.tokens:
if token.is_keyword and token.normalized == 'WITH':
with_stmt = token
break
if with_stmt is None:
# no with stmt, add one, and inject CTEs right at the beginning
first_token = parsed.token_first()
with_stmt = sqlparse.sql.Token(sqlparse.tokens.Keyword, 'with')
parsed.insert_before(first_token, with_stmt)
else:
# stmt exists, add a comma (which will come after injected CTEs)
trailing_comma = sqlparse.sql.Token(
sqlparse.tokens.Punctuation, ','
)
parsed.insert_after(with_stmt, trailing_comma)
token = sqlparse.sql.Token(
sqlparse.tokens.Keyword,
", ".join(c.sql for c in ctes)
)
parsed.insert_after(with_stmt, token)
return str(parsed)
def _recursively_prepend_ctes(
self,
model: NonSourceCompiledNode,
manifest: Manifest,
extra_context: Optional[Dict[str, Any]],
) -> Tuple[NonSourceCompiledNode, List[InjectedCTE]]:
"""This method is called by the 'compile_node' method. Starting
from the node that it is passed in, it will recursively call
itself using the 'extra_ctes'. The 'ephemeral' models do
not produce SQL that is executed directly, instead they
are rolled up into the models that refer to them by
inserting CTEs into the SQL.
"""
if model.compiled_sql is None:
raise RuntimeException(
'Cannot inject ctes into an unparsed node', model
)
if model.extra_ctes_injected:
return (model, model.extra_ctes)
# Just to make it plain that nothing is actually injected for this case
if not model.extra_ctes:
model.extra_ctes_injected = True
manifest.update_node(model)
return (model, model.extra_ctes)
# This stores the ctes which will all be recursively
# gathered and then "injected" into the model.
prepended_ctes: List[InjectedCTE] = []
# extra_ctes are added to the model by
# RuntimeRefResolver.create_relation, which adds an
# extra_cte for every model relation which is an
# ephemeral model.
for cte in model.extra_ctes:
if cte.id not in manifest.nodes:
raise InternalException(
f'During compilation, found a cte reference that '
f'could not be resolved: {cte.id}'
)
cte_model = manifest.nodes[cte.id]
if not cte_model.is_ephemeral_model:
raise InternalException(f'{cte.id} is not ephemeral')
# This model has already been compiled, so it's been
# through here before
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
# if the cte_model isn't compiled, i.e. first time here
else:
# This is an ephemeral parsed model that we can compile.
# Compile and update the node
cte_model = self._compile_node(
cte_model, manifest, extra_context)
# recursively call this method
cte_model, new_prepended_ctes = \
self._recursively_prepend_ctes(
cte_model, manifest, extra_context
)
# Save compiled SQL file and sync manifest
self._write_node(cte_model)
manifest.sync_update_node(cte_model)
_extend_prepended_ctes(prepended_ctes, new_prepended_ctes)
new_cte_name = self.add_ephemeral_prefix(cte_model.name)
rendered_sql = (
cte_model._pre_injected_sql or cte_model.compiled_sql
)
sql = f' {new_cte_name} as (\n{rendered_sql}\n)'
_add_prepended_cte(prepended_ctes, InjectedCTE(id=cte.id, sql=sql))
injected_sql = self._inject_ctes_into_sql(
model.compiled_sql,
prepended_ctes,
)
model._pre_injected_sql = model.compiled_sql
model.compiled_sql = injected_sql
model.extra_ctes_injected = True
model.extra_ctes = prepended_ctes
model.validate(model.to_dict(omit_none=True))
manifest.update_node(model)
return model, prepended_ctes
# creates a compiled_node from the ManifestNode passed in,
# creates a "context" dictionary for jinja rendering,
# and then renders the "compiled_sql" using the node, the
# raw_sql and the context.
def _compile_node(
self,
node: ManifestNode,
manifest: Manifest,
extra_context: Optional[Dict[str, Any]] = None,
) -> NonSourceCompiledNode:
if extra_context is None:
extra_context = {}
logger.debug("Compiling {}".format(node.unique_id))
data = node.to_dict()
data = node.to_dict(omit_none=True)
data.update({
'compiled': False,
'compiled_sql': None,
'extra_ctes_injected': False,
'extra_ctes': [],
'injected_sql': None,
})
compiled_node = _compiled_type_for(node).from_dict(data)
context = generate_runtime_model(
compiled_node, self.config, manifest
context = self._create_node_context(
compiled_node, manifest, extra_context
)
context.update(extra_context)
compiled_node.compiled_sql = dbt.clients.jinja.get_rendered(
compiled_node.compiled_sql = jinja.get_rendered(
node.raw_sql,
context,
node)
node,
)
compiled_node.relation_name = self._get_relation_name(node)
compiled_node.compiled = True
injected_node, _ = prepend_ctes(compiled_node, manifest)
return compiled_node
should_wrap = {NodeType.Test, NodeType.Operation}
if injected_node.resource_type in should_wrap:
# data tests get wrapped in count(*)
# TODO : move this somewhere more reasonable
if 'data' in injected_node.tags and \
injected_node.resource_type == NodeType.Test:
injected_node.wrapped_sql = (
"select count(*) as errors "
"from (\n{test_sql}\n) sbq").format(
test_sql=injected_node.injected_sql)
else:
# don't wrap schema tests or analyses.
injected_node.wrapped_sql = injected_node.injected_sql
elif injected_node.resource_type == NodeType.Snapshot:
# unfortunately we do everything automagically for
# snapshots. in the future it'd be nice to generate
# the SQL at the parser level.
pass
elif(injected_node.resource_type == NodeType.Model and
injected_node.get_materialization() == 'ephemeral'):
pass
else:
injected_node.wrapped_sql = None
return injected_node
def write_graph_file(self, linker, manifest):
def write_graph_file(self, linker: Linker, manifest: Manifest):
filename = graph_file_name
graph_path = os.path.join(self.config.target_path, filename)
if dbt.flags.WRITE_JSON:
if flags.WRITE_JSON:
linker.write_graph(graph_path, manifest)
def link_node(self, linker, node, manifest):
def link_node(
self, linker: Linker, node: GraphMemberNode, manifest: Manifest
):
linker.add_node(node.unique_id)
for dependency in node.depends_on_nodes:
if manifest.nodes.get(dependency):
if dependency in manifest.nodes:
linker.dependency(
node.unique_id,
(manifest.nodes.get(dependency).unique_id))
(manifest.nodes[dependency].unique_id)
)
elif dependency in manifest.sources:
linker.dependency(
node.unique_id,
(manifest.sources[dependency].unique_id)
)
else:
dbt.exceptions.dependency_not_found(node, dependency)
dependency_not_found(node, dependency)
def link_graph(self, linker, manifest):
def link_graph(self, linker: Linker, manifest: Manifest):
for source in manifest.sources.values():
linker.add_node(source.unique_id)
for node in manifest.nodes.values():
self.link_node(linker, node, manifest)
for exposure in manifest.exposures.values():
self.link_node(linker, exposure, manifest)
# linker.add_node(exposure.unique_id)
cycle = linker.find_cycles()
if cycle:
raise RuntimeError("Found a cycle: {}".format(cycle))
def compile(self, manifest, write=True):
def compile(self, manifest: Manifest, write=True) -> Graph:
self.initialize()
linker = Linker()
self.link_graph(linker, manifest)
@@ -226,37 +431,41 @@ class Compiler:
self.write_graph_file(linker, manifest)
print_compile_stats(stats)
return linker
return Graph(linker.graph)
# writes the "compiled_sql" into the target/compiled directory
def _write_node(self, node: NonSourceCompiledNode) -> ManifestNode:
if (not node.extra_ctes_injected or
node.resource_type == NodeType.Snapshot):
return node
logger.debug(f'Writing injected SQL for node "{node.unique_id}"')
def compile_manifest(config, manifest, write=True) -> Linker:
compiler = Compiler(config)
compiler.initialize()
return compiler.compile(manifest, write=write)
if node.compiled_sql:
node.compiled_path = node.write_node(
self.config.target_path,
'compiled',
node.compiled_sql
)
return node
def compile_node(
self,
node: ManifestNode,
manifest: Manifest,
extra_context: Optional[Dict[str, Any]] = None,
write: bool = True,
) -> NonSourceCompiledNode:
"""This is the main entry point into this code. It's called by
CompileRunner.compile, GenericRPCRunner.compile, and
RunTask.get_hook_sql. It calls '_compile_node' to convert
the node into a compiled node, and then calls the
recursive method to "prepend" the ctes.
"""
node = self._compile_node(node, manifest, extra_context)
def _is_writable(node):
if not node.injected_sql:
return False
if node.resource_type == NodeType.Snapshot:
return False
return True
def compile_node(adapter, config, node, manifest, extra_context, write=True):
compiler = Compiler(config)
node = compiler.compile_node(node, manifest, extra_context)
if write and _is_writable(node):
logger.debug('Writing injected SQL for node "{}"'.format(
node.unique_id))
node.build_path = node.write_node(
config.target_path,
'compiled',
node.injected_sql
node, _ = self._recursively_prepend_ctes(
node, manifest, extra_context
)
return node
if write:
self._write_node(node)
return node

View File

@@ -1,5 +1,4 @@
# all these are just exports, they need "noqa" so flake8 will not complain.
from .profile import Profile, PROFILES_DIR, read_user_config # noqa
from .project import Project # noqa
from .runtime import RuntimeConfig # noqa
from .renderer import ConfigRenderer # noqa
from .project import Project, IsFQNResource # noqa
from .runtime import RuntimeConfig, UnsetProfileConfig # noqa

View File

@@ -2,12 +2,13 @@ from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple
import os
from hologram import ValidationError
from dbt.dataclass_schema import ValidationError
from dbt.clients.system import load_file_contents
from dbt.clients.yaml_helper import load_yaml_text
from dbt.contracts.connection import Credentials, HasCredentials
from dbt.contracts.project import ProfileConfig, UserConfig
from dbt.exceptions import CompilationException
from dbt.exceptions import DbtProfileError
from dbt.exceptions import DbtProjectError
from dbt.exceptions import ValidationException
@@ -16,7 +17,7 @@ from dbt.exceptions import validator_error_message
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.utils import coerce_dict_str
from .renderer import ConfigRenderer
from .renderer import ProfileRenderer
DEFAULT_THREADS = 1
DEFAULT_PROFILES_DIR = os.path.join(os.path.expanduser('~'), '.dbt')
@@ -52,7 +53,15 @@ def read_profile(profiles_dir: str) -> Dict[str, Any]:
if os.path.isfile(path):
try:
contents = load_file_contents(path, strip=False)
return load_yaml_text(contents)
yaml_content = load_yaml_text(contents)
if not yaml_content:
msg = f'The profiles.yml file at {path} is empty'
raise DbtProfileError(
INVALID_PROFILE_MESSAGE.format(
error_string=msg
)
)
return yaml_content
except ValidationException as e:
msg = INVALID_PROFILE_MESSAGE.format(error_string=e)
raise ValidationException(msg) from e
@@ -66,13 +75,17 @@ def read_user_config(directory: str) -> UserConfig:
if profile:
user_cfg = coerce_dict_str(profile.get('config', {}))
if user_cfg is not None:
UserConfig.validate(user_cfg)
return UserConfig.from_dict(user_cfg)
except (RuntimeException, ValidationError):
pass
return UserConfig()
@dataclass
# The Profile class is included in RuntimeConfig, so any attribute
# additions must also be set where the RuntimeConfig class is created
# `init=False` is a workaround for https://bugs.python.org/issue45081
@dataclass(init=False)
class Profile(HasCredentials):
profile_name: str
target_name: str
@@ -80,6 +93,23 @@ class Profile(HasCredentials):
threads: int
credentials: Credentials
def __init__(
self,
profile_name: str,
target_name: str,
config: UserConfig,
threads: int,
credentials: Credentials
):
"""Explicitly defining `__init__` to work around bug in Python 3.9.7
https://bugs.python.org/issue45081
"""
self.profile_name = profile_name
self.target_name = target_name
self.config = config
self.threads = threads
self.credentials = credentials
def to_profile_info(
self, serialize_credentials: bool = False
) -> Dict[str, Any]:
@@ -99,10 +129,24 @@ class Profile(HasCredentials):
'credentials': self.credentials,
}
if serialize_credentials:
result['config'] = self.config.to_dict()
result['credentials'] = self.credentials.to_dict()
result['config'] = self.config.to_dict(omit_none=True)
result['credentials'] = self.credentials.to_dict(omit_none=True)
return result
def to_target_dict(self) -> Dict[str, Any]:
target = dict(
self.credentials.connection_info(with_aliases=True)
)
target.update({
'type': self.credentials.type,
'threads': self.threads,
'name': self.target_name,
'target_name': self.target_name,
'profile_name': self.profile_name,
'config': self.config.to_dict(omit_none=True),
})
return target
def __eq__(self, other: object) -> bool:
if not (isinstance(other, self.__class__) and
isinstance(self, other.__class__)):
@@ -112,10 +156,10 @@ class Profile(HasCredentials):
def validate(self):
try:
if self.credentials:
self.credentials.to_dict(validate=True)
ProfileConfig.from_dict(
self.to_profile_info(serialize_credentials=True)
)
dct = self.credentials.to_dict(omit_none=True)
self.credentials.validate(dct)
dct = self.to_profile_info(serialize_credentials=True)
ProfileConfig.validate(dct)
except ValidationError as exc:
raise DbtProfileError(validator_error_message(exc)) from exc
@@ -135,7 +179,9 @@ class Profile(HasCredentials):
typename = profile.pop('type')
try:
cls = load_plugin(typename)
credentials = cls.from_dict(profile)
data = cls.translate_aliases(profile)
cls.validate(data)
credentials = cls.from_dict(data)
except (RuntimeException, ValidationError) as e:
msg = str(e) if isinstance(e, RuntimeException) else e.message
raise DbtProfileError(
@@ -175,6 +221,14 @@ class Profile(HasCredentials):
.format(profile_name, target_name, outputs))
raise DbtProfileError(msg, result_type='invalid_target')
profile_data = outputs[target_name]
if not isinstance(profile_data, dict):
msg = (
f"output '{target_name}' of profile '{profile_name}' is "
f"misconfigured in profiles.yml"
)
raise DbtProfileError(msg, result_type='invalid_target')
return profile_data
@classmethod
@@ -200,6 +254,7 @@ class Profile(HasCredentials):
"""
if user_cfg is None:
user_cfg = {}
UserConfig.validate(user_cfg)
config = UserConfig.from_dict(user_cfg)
profile = cls(
@@ -218,7 +273,7 @@ class Profile(HasCredentials):
raw_profile: Dict[str, Any],
profile_name: str,
target_override: Optional[str],
renderer: ConfigRenderer,
renderer: ProfileRenderer,
) -> Tuple[str, Dict[str, Any]]:
"""This is a containment zone for the hateful way we're rendering
profiles.
@@ -246,7 +301,10 @@ class Profile(HasCredentials):
raw_profile, profile_name, target_name
)
profile_data = renderer.render_profile_data(raw_profile_data)
try:
profile_data = renderer.render_data(raw_profile_data)
except CompilationException as exc:
raise DbtProfileError(str(exc)) from exc
return target_name, profile_data
@classmethod
@@ -254,7 +312,7 @@ class Profile(HasCredentials):
cls,
raw_profile: Dict[str, Any],
profile_name: str,
renderer: ConfigRenderer,
renderer: ProfileRenderer,
user_cfg: Optional[Dict[str, Any]] = None,
target_override: Optional[str] = None,
threads_override: Optional[int] = None,
@@ -280,7 +338,6 @@ class Profile(HasCredentials):
# user_cfg is not rendered.
if user_cfg is None:
user_cfg = raw_profile.get('config')
# TODO: should it be, and the values coerced to bool?
target_name, profile_data = cls.render_profile(
raw_profile, profile_name, target_override, renderer
@@ -309,7 +366,7 @@ class Profile(HasCredentials):
cls,
raw_profiles: Dict[str, Any],
profile_name: str,
renderer: ConfigRenderer,
renderer: ProfileRenderer,
target_override: Optional[str] = None,
threads_override: Optional[int] = None,
) -> 'Profile':
@@ -335,7 +392,15 @@ class Profile(HasCredentials):
# First, we've already got our final decision on profile name, and we
# don't render keys, so we can pluck that out
raw_profile = raw_profiles[profile_name]
if not raw_profile:
msg = (
f'Profile {profile_name} in profiles.yml is empty'
)
raise DbtProfileError(
INVALID_PROFILE_MESSAGE.format(
error_string=msg
)
)
user_cfg = raw_profiles.get('config')
return cls.from_raw_profile_info(
@@ -351,7 +416,7 @@ class Profile(HasCredentials):
def render_from_args(
cls,
args: Any,
renderer: ConfigRenderer,
renderer: ProfileRenderer,
project_profile_name: Optional[str],
) -> 'Profile':
"""Given the raw profiles as read from disk and the name of the desired
@@ -373,7 +438,6 @@ class Profile(HasCredentials):
raw_profiles = read_profile(args.profiles_dir)
profile_name = cls.pick_profile_name(getattr(args, 'profile', None),
project_profile_name)
return cls.from_raw_profiles(
raw_profiles=raw_profiles,
profile_name=profile_name,

View File

@@ -1,7 +1,11 @@
from copy import deepcopy
from dataclasses import dataclass
from dataclasses import dataclass, field
from itertools import chain
from typing import List, Dict, Any, Optional, TypeVar, Union, Tuple, Callable
from typing import (
List, Dict, Any, Optional, TypeVar, Union, Mapping,
)
from typing_extensions import Protocol, runtime_checkable
import hashlib
import os
@@ -9,35 +13,31 @@ 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.contracts.connection import QueryComment
from dbt.exceptions import DbtProjectError
from dbt.exceptions import RecursionException
from dbt.exceptions import SemverException
from dbt.exceptions import validator_error_message
from dbt.exceptions import warn_or_error
from dbt.exceptions import RuntimeException
from dbt.graph import SelectionSpec
from dbt.helper_types import NoValue
from dbt.semver import VersionSpecifier
from dbt.semver import versions_compatible
from dbt.version import get_installed_version
from dbt.ui import printer
from dbt.utils import deep_map
from dbt.source_config import SourceConfig
from dbt.utils import MultiDict
from dbt.node_types import NodeType
from dbt.config.selectors import SelectorDict
from dbt.contracts.project import (
Project as ProjectContract,
SemverString,
)
from dbt.contracts.project import PackageConfig
from hologram import ValidationError
from .renderer import ConfigRenderer
UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE = """\
WARNING: Configuration paths exist in your dbt_project.yml file which do not \
apply to any resources.
There are {} unused configuration paths:\n{}
"""
from dbt.dataclass_schema import ValidationError
from .renderer import DbtProjectYamlRenderer
from .selectors import (
selector_config_from_data,
selector_data_from_root,
SelectorConfig,
)
INVALID_VERSION_ERROR = """\
@@ -69,23 +69,11 @@ Validator Error:
"""
def _list_if_none(value):
if value is None:
value = []
return value
def _dict_if_none(value):
if value is None:
value = {}
return value
def _list_if_none_or_string(value):
value = _list_if_none(value)
if isinstance(value, str):
return [value]
return value
@runtime_checkable
class IsFQNResource(Protocol):
fqn: List[str]
resource_type: NodeType
package_name: str
def _load_yaml(path):
@@ -93,32 +81,6 @@ def _load_yaml(path):
return load_yaml_text(contents)
def _get_config_paths(config, path=(), paths=None):
if paths is None:
paths = set()
for key, value in config.items():
if isinstance(value, dict):
if key in SourceConfig.ConfigKeys:
if path not in paths:
paths.add(path)
else:
_get_config_paths(value, path + (key,), paths)
else:
if path not in paths:
paths.add(path)
return frozenset(paths)
def _is_config_used(path, fqns):
if fqns:
for fqn in fqns:
if len(path) <= len(fqn) and fqn[:len(path)] == path:
return True
return False
def package_data_from_root(project_root):
package_filepath = resolve_path_from_base(
'packages.yml', project_root
@@ -131,11 +93,12 @@ def package_data_from_root(project_root):
return packages_dict
def package_config_from_data(packages_data):
if packages_data is None:
def package_config_from_data(packages_data: Dict[str, Any]):
if not packages_data:
packages_data = {'packages': []}
try:
PackageConfig.validate(packages_data)
packages = PackageConfig.from_dict(packages_data)
except ValidationError as e:
raise DbtProjectError(
@@ -202,111 +165,151 @@ def _raw_project_from(project_root: str) -> Dict[str, Any]:
return project_dict
@dataclass
class PartialProject:
profile_name: Optional[str]
project_name: Optional[str]
project_root: str
project_dict: Dict[str, Any]
def _query_comment_from_cfg(
cfg_query_comment: Union[QueryComment, NoValue, str, None]
) -> QueryComment:
if not cfg_query_comment:
return QueryComment(comment='')
def render(self, renderer):
packages_dict = package_data_from_root(self.project_root)
return Project.render_from_dict(
self.project_root,
self.project_dict,
packages_dict,
renderer,
if isinstance(cfg_query_comment, str):
return QueryComment(comment=cfg_query_comment)
if isinstance(cfg_query_comment, NoValue):
return QueryComment()
return cfg_query_comment
def validate_version(dbt_version: List[VersionSpecifier], project_name: str):
"""Ensure this package works with the installed version of dbt."""
installed = get_installed_version()
if not versions_compatible(*dbt_version):
msg = IMPOSSIBLE_VERSION_ERROR.format(
package=project_name,
version_spec=[
x.to_version_string() for x in dbt_version
]
)
raise DbtProjectError(msg)
@dataclass
class Project:
project_name: str
version: Union[SemverString, float]
project_root: str
profile_name: str
source_paths: List[str]
macro_paths: List[str]
data_paths: List[str]
test_paths: List[str]
analysis_paths: List[str]
docs_paths: List[str]
target_path: str
snapshot_paths: List[str]
clean_targets: List[str]
log_path: str
modules_path: str
quoting: Dict[str, Any]
models: Dict[str, Any]
on_run_start: List[str]
on_run_end: List[str]
seeds: Dict[str, Any]
snapshots: Dict[str, Any]
dbt_version: List[VersionSpecifier]
packages: Dict[str, Any]
query_comment: Optional[Union[str, NoValue]]
@property
def all_source_paths(self) -> List[str]:
return _all_source_paths(
self.source_paths, self.data_paths, self.snapshot_paths,
self.analysis_paths, self.macro_paths
if not versions_compatible(installed, *dbt_version):
msg = INVALID_VERSION_ERROR.format(
package=project_name,
installed=installed.to_version_string(),
version_spec=[
x.to_version_string() for x in dbt_version
]
)
raise DbtProjectError(msg)
@staticmethod
def _preprocess(project_dict: Dict[str, Any]) -> Dict[str, Any]:
"""Pre-process certain special keys to convert them from None values
into empty containers, and to turn strings into arrays of strings.
"""
handlers: Dict[Tuple[str, ...], Callable[[Any], Any]] = {
('on-run-start',): _list_if_none_or_string,
('on-run-end',): _list_if_none_or_string,
}
for k in ('models', 'seeds', 'snapshots'):
handlers[(k,)] = _dict_if_none
handlers[(k, 'vars')] = _dict_if_none
handlers[(k, 'pre-hook')] = _list_if_none_or_string
handlers[(k, 'post-hook')] = _list_if_none_or_string
handlers[('seeds', 'column_types')] = _dict_if_none
def _get_required_version(
project_dict: Dict[str, Any],
verify_version: bool,
) -> List[VersionSpecifier]:
dbt_raw_version: Union[List[str], str] = '>=0.0.0'
required = project_dict.get('require-dbt-version')
if required is not None:
dbt_raw_version = required
def converter(value: Any, keypath: Tuple[str, ...]) -> Any:
if keypath in handlers:
handler = handlers[keypath]
return handler(value)
else:
return value
try:
dbt_version = _parse_versions(dbt_raw_version)
except SemverException as e:
raise DbtProjectError(str(e)) from e
return deep_map(converter, project_dict)
@classmethod
def from_project_config(
cls,
project_dict: Dict[str, Any],
packages_dict: Optional[Dict[str, Any]] = None,
) -> 'Project':
"""Create a project from its project and package configuration, as read
by yaml.safe_load().
:param project_dict: The dictionary as read from disk
:param packages_dict: If it exists, the packages file as
read from disk.
:raises DbtProjectError: If the project is missing or invalid, or if
the packages file exists and is invalid.
:returns: The project, with defaults populated.
"""
try:
project_dict = cls._preprocess(project_dict)
except RecursionException:
if verify_version:
# no name is also an error that we want to raise
if 'name' not in project_dict:
raise DbtProjectError(
'Cycle detected: Project input has a reference to itself',
project=project_dict
'Required "name" field not present in project',
)
validate_version(dbt_version, project_dict['name'])
return dbt_version
@dataclass
class RenderComponents:
project_dict: Dict[str, Any] = field(
metadata=dict(description='The project dictionary')
)
packages_dict: Dict[str, Any] = field(
metadata=dict(description='The packages dictionary')
)
selectors_dict: Dict[str, Any] = field(
metadata=dict(description='The selectors dictionary')
)
@dataclass
class PartialProject(RenderComponents):
profile_name: Optional[str] = field(metadata=dict(
description='The unrendered profile name in the project, if set'
))
project_name: Optional[str] = field(metadata=dict(
description=(
'The name of the project. This should always be set and will not '
'be rendered'
)
))
project_root: str = field(
metadata=dict(description='The root directory of the project'),
)
verify_version: bool = field(
metadata=dict(description=(
'If True, verify the dbt version matches the required version'
))
)
def render_profile_name(self, renderer) -> Optional[str]:
if self.profile_name is None:
return None
return renderer.render_value(self.profile_name)
def get_rendered(
self,
renderer: DbtProjectYamlRenderer,
) -> RenderComponents:
rendered_project = renderer.render_project(
self.project_dict, self.project_root
)
rendered_packages = renderer.render_packages(self.packages_dict)
rendered_selectors = renderer.render_selectors(self.selectors_dict)
return RenderComponents(
project_dict=rendered_project,
packages_dict=rendered_packages,
selectors_dict=rendered_selectors,
)
def render(self, renderer: DbtProjectYamlRenderer) -> 'Project':
try:
cfg = ProjectContract.from_dict(project_dict)
rendered = self.get_rendered(renderer)
return self.create_project(rendered)
except DbtProjectError as exc:
if exc.path is None:
exc.path = os.path.join(self.project_root, 'dbt_project.yml')
raise
def create_project(self, rendered: RenderComponents) -> 'Project':
unrendered = RenderComponents(
project_dict=self.project_dict,
packages_dict=self.packages_dict,
selectors_dict=self.selectors_dict,
)
dbt_version = _get_required_version(
rendered.project_dict,
verify_version=self.verify_version,
)
try:
ProjectContract.validate(rendered.project_dict)
cfg = ProjectContract.from_dict(
rendered.project_dict
)
except ValidationError as e:
raise DbtProjectError(validator_error_message(e)) from e
# name/version are required in the Project definition, so we can assume
# they are present
name = cfg.name
@@ -334,6 +337,7 @@ class Project:
)
docs_paths: List[str] = value_or(cfg.docs_paths, all_source_paths)
asset_paths: List[str] = value_or(cfg.asset_paths, [])
target_path: str = value_or(cfg.target_path, 'target')
clean_targets: List[str] = value_or(cfg.clean_targets, [target_path])
log_path: str = value_or(cfg.log_path, 'logs')
@@ -343,32 +347,43 @@ class Project:
# break many things
quoting: Dict[str, Any] = {}
if cfg.quoting is not None:
quoting = cfg.quoting.to_dict()
quoting = cfg.quoting.to_dict(omit_none=True)
models: Dict[str, Any] = cfg.models
seeds: Dict[str, Any] = cfg.seeds
snapshots: Dict[str, Any] = cfg.snapshots
dispatch: List[Dict[str, Any]]
models: Dict[str, Any]
seeds: Dict[str, Any]
snapshots: Dict[str, Any]
sources: Dict[str, Any]
tests: Dict[str, Any]
vars_value: VarProvider
dispatch = cfg.dispatch
models = cfg.models
seeds = cfg.seeds
snapshots = cfg.snapshots
sources = cfg.sources
tests = cfg.tests
if cfg.vars is None:
vars_dict: Dict[str, Any] = {}
else:
vars_dict = cfg.vars
vars_value = VarProvider(vars_dict)
on_run_start: List[str] = value_or(cfg.on_run_start, [])
on_run_end: List[str] = value_or(cfg.on_run_end, [])
# weird type handling: no value_or use
dbt_raw_version: Union[List[str], str] = '>=0.0.0'
if cfg.require_dbt_version is not None:
dbt_raw_version = cfg.require_dbt_version
query_comment = cfg.query_comment
query_comment = _query_comment_from_cfg(cfg.query_comment)
try:
dbt_version = _parse_versions(dbt_raw_version)
except SemverException as e:
raise DbtProjectError(str(e)) from e
packages = package_config_from_data(rendered.packages_dict)
selectors = selector_config_from_data(rendered.selectors_dict)
manifest_selectors: Dict[str, Any] = {}
if rendered.selectors_dict and rendered.selectors_dict['selectors']:
# this is a dict with a single key 'selectors' pointing to a list
# of dicts.
manifest_selectors = SelectorDict.parse_from_selectors_list(
rendered.selectors_dict['selectors'])
try:
packages = package_config_from_data(packages_dict)
except ValidationError as e:
raise DbtProjectError(validator_error_message(e)) from e
project = cls(
project = Project(
project_name=name,
version=version,
project_root=project_root,
@@ -379,6 +394,7 @@ class Project:
test_paths=test_paths,
analysis_paths=analysis_paths,
docs_paths=docs_paths,
asset_paths=asset_paths,
target_path=target_path,
snapshot_paths=snapshot_paths,
clean_targets=clean_targets,
@@ -388,16 +404,139 @@ class Project:
models=models,
on_run_start=on_run_start,
on_run_end=on_run_end,
dispatch=dispatch,
seeds=seeds,
snapshots=snapshots,
dbt_version=dbt_version,
packages=packages,
manifest_selectors=manifest_selectors,
selectors=selectors,
query_comment=query_comment,
sources=sources,
tests=tests,
vars=vars_value,
config_version=cfg.config_version,
unrendered=unrendered,
)
# sanity check - this means an internal issue
project.validate()
return project
@classmethod
def from_dicts(
cls,
project_root: str,
project_dict: Dict[str, Any],
packages_dict: Dict[str, Any],
selectors_dict: Dict[str, Any],
*,
verify_version: bool = False,
):
"""Construct a partial project from its constituent dicts.
"""
project_name = project_dict.get('name')
profile_name = project_dict.get('profile')
return cls(
profile_name=profile_name,
project_name=project_name,
project_root=project_root,
project_dict=project_dict,
packages_dict=packages_dict,
selectors_dict=selectors_dict,
verify_version=verify_version,
)
@classmethod
def from_project_root(
cls, project_root: str, *, verify_version: bool = False
) -> 'PartialProject':
project_root = os.path.normpath(project_root)
project_dict = _raw_project_from(project_root)
config_version = project_dict.get('config-version', 1)
if config_version != 2:
raise DbtProjectError(
f'Invalid config version: {config_version}, expected 2',
path=os.path.join(project_root, 'dbt_project.yml')
)
packages_dict = package_data_from_root(project_root)
selectors_dict = selector_data_from_root(project_root)
return cls.from_dicts(
project_root=project_root,
project_dict=project_dict,
selectors_dict=selectors_dict,
packages_dict=packages_dict,
verify_version=verify_version,
)
class VarProvider:
"""Var providers are tied to a particular Project."""
def __init__(
self,
vars: Dict[str, Dict[str, Any]]
) -> None:
self.vars = vars
def vars_for(
self, node: IsFQNResource, adapter_type: str
) -> Mapping[str, Any]:
# in v2, vars are only either project or globally scoped
merged = MultiDict([self.vars])
merged.add(self.vars.get(node.package_name, {}))
return merged
def to_dict(self):
return self.vars
# The Project class is included in RuntimeConfig, so any attribute
# additions must also be set where the RuntimeConfig class is created
@dataclass
class Project:
project_name: str
version: Union[SemverString, float]
project_root: str
profile_name: Optional[str]
source_paths: List[str]
macro_paths: List[str]
data_paths: List[str]
test_paths: List[str]
analysis_paths: List[str]
docs_paths: List[str]
asset_paths: List[str]
target_path: str
snapshot_paths: List[str]
clean_targets: List[str]
log_path: str
modules_path: str
quoting: Dict[str, Any]
models: Dict[str, Any]
on_run_start: List[str]
on_run_end: List[str]
dispatch: List[Dict[str, Any]]
seeds: Dict[str, Any]
snapshots: Dict[str, Any]
sources: Dict[str, Any]
tests: Dict[str, Any]
vars: VarProvider
dbt_version: List[VersionSpecifier]
packages: Dict[str, Any]
manifest_selectors: Dict[str, Any]
selectors: SelectorConfig
query_comment: QueryComment
config_version: int
unrendered: RenderComponents
@property
def all_source_paths(self) -> List[str]:
return _all_source_paths(
self.source_paths, self.data_paths, self.snapshot_paths,
self.analysis_paths, self.macro_paths
)
def __str__(self):
cfg = self.to_project_config(with_packages=True)
return str(cfg)
@@ -428,6 +567,7 @@ class Project:
'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,
@@ -436,124 +576,85 @@ class Project:
'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,
'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())
if self.query_comment != NoValue():
result['query-comment'] = self.query_comment
result.update(self.packages.to_dict(omit_none=True))
return result
def validate(self):
try:
ProjectContract.from_dict(self.to_project_config())
ProjectContract.validate(self.to_project_config())
except ValidationError as e:
raise DbtProjectError(validator_error_message(e)) from e
@classmethod
def partial_load(
cls, project_root: str, *, verify_version: bool = False
) -> PartialProject:
return PartialProject.from_project_root(
project_root,
verify_version=verify_version,
)
@classmethod
def render_from_dict(
cls,
project_root: str,
project_dict: Dict[str, Any],
packages_dict: Dict[str, Any],
renderer: ConfigRenderer,
selectors_dict: Dict[str, Any],
renderer: DbtProjectYamlRenderer,
*,
verify_version: bool = False
) -> 'Project':
rendered_project = renderer.render_project(project_dict)
rendered_project['project-root'] = project_root
rendered_packages = renderer.render_packages_data(packages_dict)
return cls.from_project_config(rendered_project, rendered_packages)
@classmethod
def partial_load(
cls, project_root: str
) -> PartialProject:
project_root = os.path.normpath(project_root)
project_dict = _raw_project_from(project_root)
project_name = project_dict.get('name')
profile_name = project_dict.get('profile')
return PartialProject(
profile_name=profile_name,
project_name=project_name,
partial = PartialProject.from_dicts(
project_root=project_root,
project_dict=project_dict,
packages_dict=packages_dict,
selectors_dict=selectors_dict,
verify_version=verify_version,
)
return partial.render(renderer)
@classmethod
def from_project_root(
cls, project_root: str, renderer: ConfigRenderer
cls,
project_root: str,
renderer: DbtProjectYamlRenderer,
*,
verify_version: bool = False,
) -> 'Project':
partial = cls.partial_load(project_root)
partial = cls.partial_load(project_root, verify_version=verify_version)
return partial.render(renderer)
def hashed_name(self):
return hashlib.md5(self.project_name.encode('utf-8')).hexdigest()
def get_resource_config_paths(self):
"""Return a dictionary with 'seeds' and 'models' keys whose values are
lists of lists of strings, where each inner list of strings represents
a configured path in the resource.
"""
return {
'models': _get_config_paths(self.models),
'seeds': _get_config_paths(self.seeds),
'snapshots': _get_config_paths(self.snapshots),
}
def get_unused_resource_config_paths(self, resource_fqns, disabled):
"""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
used.
"""
disabled_fqns = frozenset(tuple(fqn) for fqn in disabled)
resource_config_paths = self.get_resource_config_paths()
unused_resource_config_paths = []
for resource_type, config_paths in resource_config_paths.items():
used_fqns = resource_fqns.get(resource_type, frozenset())
fqns = used_fqns | disabled_fqns
for config_path in config_paths:
if not _is_config_used(config_path, fqns):
unused_resource_config_paths.append(
(resource_type,) + config_path
)
return unused_resource_config_paths
def warn_for_unused_resource_config_paths(self, resource_fqns, disabled):
unused = self.get_unused_resource_config_paths(resource_fqns, disabled)
if len(unused) == 0:
return
msg = UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE.format(
len(unused),
'\n'.join('- {}'.format('.'.join(u)) for u in unused)
)
warn_or_error(msg, log_fmt=printer.yellow('{}'))
def validate_version(self):
"""Ensure this package works with the installed version of dbt."""
installed = get_installed_version()
if not versions_compatible(*self.dbt_version):
msg = IMPOSSIBLE_VERSION_ERROR.format(
package=self.project_name,
version_spec=[
x.to_version_string() for x in self.dbt_version
]
def get_selector(self, name: str) -> SelectionSpec:
if name not in self.selectors:
raise RuntimeException(
f'Could not find selector named {name}, expected one of '
f'{list(self.selectors)}'
)
raise DbtProjectError(msg)
return self.selectors[name]
if not versions_compatible(installed, *self.dbt_version):
msg = INVALID_VERSION_ERROR.format(
package=self.project_name,
installed=installed.to_version_string(),
version_spec=[
x.to_version_string() for x in self.dbt_version
]
)
raise DbtProjectError(msg)
def get_macro_search_order(self, macro_namespace: str):
for dispatch_entry in self.dispatch:
if dispatch_entry['macro_namespace'] == macro_namespace:
return dispatch_entry['search_order']
return None

View File

@@ -1,134 +1,238 @@
from typing import Dict, Any
from typing import Dict, Any, Tuple, Optional, Union, Callable
from dbt.clients.jinja import get_rendered
from dbt.clients.jinja import get_rendered, catch_jinja
from dbt.exceptions import DbtProfileError
from dbt.exceptions import DbtProjectError
from dbt.exceptions import RecursionException
from dbt.exceptions import (
DbtProjectError, CompilationException, RecursionException
)
from dbt.node_types import NodeType
from dbt.utils import deep_map
class ConfigRenderer:
"""A renderer provides configuration rendering for a given set of cli
variables and a render type.
"""
def __init__(self, context: Dict[str, Any]):
Keypath = Tuple[Union[str, int], ...]
class BaseRenderer:
def __init__(self, context: Dict[str, Any]) -> None:
self.context = context
@staticmethod
def _is_deferred_render(keypath):
if not keypath:
return False
@property
def name(self):
return 'Rendering'
first = keypath[0]
# run hooks
if first in {'on-run-start', 'on-run-end', 'query-comment'}:
return True
# models have two things to avoid
if first in {'seeds', 'models', 'snapshots'}:
# model-level hooks
if 'pre-hook' in keypath or 'post-hook' in keypath:
return True
# model-level 'vars' declarations
if 'vars' in keypath:
return True
def should_render_keypath(self, keypath: Keypath) -> bool:
return True
return False
def _render_project_entry(self, value, keypath):
"""Render an entry, in case it's jinja. This is meant to be passed to
deep_map.
If the parsed entry is a string and has the name 'port', this will
attempt to cast it to an int, and on failure will return the parsed
string.
:param value Any: The value to potentially render
:param key str: The key to convert on.
:return Any: The rendered entry.
"""
# query comments and hooks should be treated as raw sql, they'll get
# rendered later.
# Same goes for 'vars' declarations inside 'models'/'seeds'
if self._is_deferred_render(keypath):
def render_entry(self, value: Any, keypath: Keypath) -> Any:
if not self.should_render_keypath(keypath):
return value
return self.render_value(value)
return self.render_value(value, keypath)
def render_value(self, value, keypath=None):
def render_value(
self, value: Any, keypath: Optional[Keypath] = None
) -> Any:
# keypath is ignored.
# if it wasn't read as a string, ignore it
if not isinstance(value, str):
return value
return str(get_rendered(value, self.context))
def _render_profile_data(self, value, keypath):
result = self.render_value(value)
if len(keypath) == 1 and keypath[-1] == 'port':
try:
result = int(result)
except ValueError:
# let the validator or connection handle this
pass
return result
@staticmethod
def _is_schema_test(keypath) -> bool:
# we got passed an UnparsedSourceDefinition
if len(keypath) > 2 and keypath[0] == 'tables':
if keypath[2] == 'tests':
return True
elif keypath[2] == 'columns':
if len(keypath) > 4 and keypath[4] == 'tests':
return True
return False
def _render_schema_source_data(self, value, keypath):
# things to not render:
# - descriptions
# - test arguments
if len(keypath) > 0 and keypath[-1] == 'description':
return value
elif self._is_schema_test(keypath):
return value
return self.render_value(value)
def render_project(self, as_parsed):
"""Render the parsed data, returning a new dict (or whatever was read).
"""
try:
return deep_map(self._render_project_entry, as_parsed)
with catch_jinja():
return get_rendered(value, self.context, native=True)
except CompilationException as exc:
msg = f'Could not render {value}: {exc.msg}'
raise CompilationException(msg) from exc
def render_data(
self, data: Dict[str, Any]
) -> Dict[str, Any]:
try:
return deep_map(self.render_entry, data)
except RecursionException:
raise DbtProjectError(
'Cycle detected: Project input has a reference to itself',
project=as_parsed
f'Cycle detected: {self.name} input has a reference to itself',
project=data
)
def render_profile_data(self, as_parsed):
"""Render the chosen profile entry, as it was parsed."""
try:
return deep_map(self._render_profile_data, as_parsed)
except RecursionException:
raise DbtProfileError(
'Cycle detected: Profile input has a reference to itself',
project=as_parsed
)
def render_schema_source(self, as_parsed):
try:
return deep_map(self._render_schema_source_data, as_parsed)
except RecursionException:
raise DbtProfileError(
'Cycle detected: schema.yml input has a reference to itself',
project=as_parsed
)
def _list_if_none(value):
if value is None:
value = []
return value
def render_packages_data(self, as_parsed):
try:
return deep_map(self.render_value, as_parsed)
except RecursionException:
raise DbtProfileError(
'Cycle detected: schema.yml input has a reference to itself',
project=as_parsed
)
def _dict_if_none(value):
if value is None:
value = {}
return value
def _list_if_none_or_string(value):
value = _list_if_none(value)
if isinstance(value, str):
return [value]
return value
class ProjectPostprocessor(Dict[Keypath, Callable[[Any], Any]]):
def __init__(self):
super().__init__()
self[('on-run-start',)] = _list_if_none_or_string
self[('on-run-end',)] = _list_if_none_or_string
for k in ('models', 'seeds', 'snapshots'):
self[(k,)] = _dict_if_none
self[(k, 'vars')] = _dict_if_none
self[(k, 'pre-hook')] = _list_if_none_or_string
self[(k, 'post-hook')] = _list_if_none_or_string
self[('seeds', 'column_types')] = _dict_if_none
def postprocess(self, value: Any, key: Keypath) -> Any:
if key in self:
handler = self[key]
return handler(value)
return value
class DbtProjectYamlRenderer(BaseRenderer):
_KEYPATH_HANDLERS = ProjectPostprocessor()
@property
def name(self):
'Project config'
def get_package_renderer(self) -> BaseRenderer:
return PackageRenderer(self.context)
def get_selector_renderer(self) -> BaseRenderer:
return SelectorRenderer(self.context)
def render_project(
self,
project: Dict[str, Any],
project_root: str,
) -> Dict[str, Any]:
"""Render the project and insert the project root after rendering."""
rendered_project = self.render_data(project)
rendered_project['project-root'] = project_root
return rendered_project
def render_packages(self, packages: Dict[str, Any]):
"""Render the given packages dict"""
package_renderer = self.get_package_renderer()
return package_renderer.render_data(packages)
def render_selectors(self, selectors: Dict[str, Any]):
selector_renderer = self.get_selector_renderer()
return selector_renderer.render_data(selectors)
def render_entry(self, value: Any, keypath: Keypath) -> Any:
result = super().render_entry(value, keypath)
return self._KEYPATH_HANDLERS.postprocess(result, keypath)
def should_render_keypath(self, keypath: Keypath) -> bool:
if not keypath:
return True
first = keypath[0]
# run hooks are not rendered
if first in {'on-run-start', 'on-run-end', 'query-comment'}:
return False
# don't render vars blocks until runtime
if first == 'vars':
return False
if first in {'seeds', 'models', 'snapshots', 'tests'}:
keypath_parts = {
(k.lstrip('+ ') if isinstance(k, str) else k)
for k in keypath
}
# model-level hooks
if 'pre-hook' in keypath_parts or 'post-hook' in keypath_parts:
return False
return True
class ProfileRenderer(BaseRenderer):
@property
def name(self):
'Profile'
class SchemaYamlRenderer(BaseRenderer):
DOCUMENTABLE_NODES = frozenset(
n.pluralize() for n in NodeType.documentable()
)
@property
def name(self):
return 'Rendering yaml'
def _is_norender_key(self, keypath: Keypath) -> bool:
"""
models:
- name: blah
- description: blah
tests: ...
- columns:
- name:
- description: blah
tests: ...
Return True if it's tests or description - those aren't rendered
"""
if len(keypath) >= 2 and keypath[1] in ('tests', 'description'):
return True
if (
len(keypath) >= 4 and
keypath[1] == 'columns' and
keypath[3] in ('tests', 'description')
):
return True
return False
# don't render descriptions or test keyword arguments
def should_render_keypath(self, keypath: Keypath) -> bool:
if len(keypath) < 2:
return True
if keypath[0] not in self.DOCUMENTABLE_NODES:
return True
if len(keypath) < 3:
return True
if keypath[0] == NodeType.Source.pluralize():
if keypath[2] == 'description':
return False
if keypath[2] == 'tables':
if self._is_norender_key(keypath[3:]):
return False
elif keypath[0] == NodeType.Macro.pluralize():
if keypath[2] == 'arguments':
if self._is_norender_key(keypath[3:]):
return False
elif self._is_norender_key(keypath[1:]):
return False
else: # keypath[0] in self.DOCUMENTABLE_NODES:
if self._is_norender_key(keypath[1:]):
return False
return True
class PackageRenderer(BaseRenderer):
@property
def name(self):
return 'Packages config'
class SelectorRenderer(BaseRenderer):
@property
def name(self):
return 'Selector config'

View File

@@ -1,35 +1,71 @@
from copy import deepcopy
from dataclasses import dataclass
import itertools
import os
from typing import Dict, Any
from copy import deepcopy
from dataclasses import dataclass, fields
from pathlib import Path
from typing import (
Dict, Any, Optional, Mapping, Iterator, Iterable, Tuple, List, MutableSet,
Type
)
from .profile import Profile
from .project import Project
from .renderer import ConfigRenderer
from dbt.utils import parse_cli_vars
from .renderer import DbtProjectYamlRenderer, ProfileRenderer
from .utils import parse_cli_vars
from dbt import tracking
from dbt.adapters.factory import get_relation_class_by_name, get_include_paths
from dbt.helper_types import FQNPath, PathSet
from dbt.context.base import generate_base_context
from dbt.context.target import generate_target_context
from dbt.contracts.connection import AdapterRequiredConfig
from dbt.contracts.connection import AdapterRequiredConfig, Credentials
from dbt.contracts.graph.manifest import ManifestMetadata
from dbt.contracts.project import Configuration
from dbt.exceptions import DbtProjectError
from dbt.exceptions import validator_error_message
from dbt.adapters.factory import get_relation_class_by_name
from dbt.contracts.relation import ComponentName
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.ui import warning_tag
from hologram import ValidationError
from dbt.contracts.project import Configuration, UserConfig
from dbt.exceptions import (
RuntimeException,
DbtProfileError,
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]:
src: Dict[str, Any] = profile.credentials.translate_aliases(proj.quoting)
result: Dict[ComponentName, bool] = {}
for key in ComponentName:
if key in src:
value = src[key]
if isinstance(value, bool):
result[key] = value
return result
@dataclass
class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
args: Any
profile_name: str
cli_vars: Dict[str, Any]
dependencies: Optional[Mapping[str, 'RuntimeConfig']] = None
def __post_init__(self):
self.validate()
@classmethod
def from_parts(
cls, project: Project, profile: Profile, args: Any,
cls,
project: Project,
profile: Profile,
args: Any,
dependencies: Optional[Mapping[str, 'RuntimeConfig']] = None,
) -> 'RuntimeConfig':
"""Instantiate a RuntimeConfig from its components.
@@ -41,8 +77,8 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
quoting: Dict[str, Any] = (
get_relation_class_by_name(profile.credentials.type)
.get_default_quote_policy()
.replace_dict(project.quoting)
).to_dict()
.replace_dict(_project_quoting_dict(project, profile))
).to_dict(omit_none=True)
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, 'vars', '{}'))
@@ -56,6 +92,7 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
test_paths=project.test_paths,
analysis_paths=project.analysis_paths,
docs_paths=project.docs_paths,
asset_paths=project.asset_paths,
target_path=project.target_path,
snapshot_paths=project.snapshot_paths,
clean_targets=project.clean_targets,
@@ -65,11 +102,19 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
models=project.models,
on_run_start=project.on_run_start,
on_run_end=project.on_run_end,
dispatch=project.dispatch,
seeds=project.seeds,
snapshots=project.snapshots,
dbt_version=project.dbt_version,
packages=project.packages,
manifest_selectors=project.manifest_selectors,
selectors=project.selectors,
query_comment=project.query_comment,
sources=project.sources,
tests=project.tests,
vars=project.vars,
config_version=project.config_version,
unrendered=project.unrendered,
profile_name=profile.profile_name,
target_name=profile.target_name,
config=profile.config,
@@ -77,6 +122,7 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
credentials=profile.credentials,
args=args,
cli_vars=cli_vars,
dependencies=dependencies,
)
def new_project(self, project_root: str) -> 'RuntimeConfig':
@@ -93,9 +139,13 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
profile.validate()
# load the new project and its packages. Don't pass cli variables.
renderer = ConfigRenderer(generate_target_context(profile, {}))
renderer = DbtProjectYamlRenderer(generate_target_context(profile, {}))
project = Project.from_project_root(project_root, renderer)
project = Project.from_project_root(
project_root,
renderer,
verify_version=getattr(self.args, 'version_check', False),
)
cfg = self.from_parts(
project=project,
@@ -126,12 +176,48 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
:raises DbtProjectError: If the configuration fails validation.
"""
try:
Configuration.from_dict(self.serialize())
Configuration.validate(self.serialize())
except ValidationError as e:
raise DbtProjectError(validator_error_message(e)) from e
if getattr(self.args, 'version_check', False):
self.validate_version()
@classmethod
def _get_rendered_profile(
cls,
args: Any,
profile_renderer: ProfileRenderer,
profile_name: Optional[str],
) -> Profile:
return Profile.render_from_args(
args, profile_renderer, profile_name
)
@classmethod
def collect_parts(
cls: Type['RuntimeConfig'], args: Any
) -> Tuple[Project, Profile]:
# profile_name from the project
project_root = args.project_dir if args.project_dir else os.getcwd()
version_check = getattr(args, 'version_check', False)
partial = Project.partial_load(
project_root,
verify_version=version_check
)
# build the profile using the base renderer and the one fact we know
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, 'vars', '{}'))
profile_renderer = ProfileRenderer(generate_base_context(cli_vars))
profile_name = partial.render_profile_name(profile_renderer)
profile = cls._get_rendered_profile(
args, profile_renderer, profile_name
)
# get a new renderer using our target information and render the
# project
ctx = generate_target_context(profile, cli_vars)
project_renderer = DbtProjectYamlRenderer(ctx)
project = partial.render(project_renderer)
return (project, profile)
@classmethod
def from_args(cls, args: Any) -> 'RuntimeConfig':
@@ -144,20 +230,7 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
:raises DbtProfileError: If the profile is invalid or missing.
:raises ValidationException: If the cli variables are invalid.
"""
# profile_name from the project
partial = Project.partial_load(os.getcwd())
# build the profile using the base renderer and the one fact we know
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, 'vars', '{}'))
renderer = ConfigRenderer(generate_base_context(cli_vars=cli_vars))
profile = Profile.render_from_args(
args, renderer, partial.profile_name
)
# get a new renderer using our target information and render the
# project
renderer = ConfigRenderer(generate_target_context(profile, cli_vars))
project = partial.render(renderer)
project, profile = cls.collect_parts(args)
return cls.from_parts(
project=project,
@@ -170,3 +243,343 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
project_id=self.hashed_name(),
adapter_type=self.credentials.type
)
def _get_v2_config_paths(
self,
config,
path: FQNPath,
paths: MutableSet[FQNPath],
) -> PathSet:
for key, value in config.items():
if isinstance(value, dict) and not key.startswith('+'):
self._get_v2_config_paths(value, path + (key,), paths)
else:
paths.add(path)
return frozenset(paths)
def _get_config_paths(
self,
config: Dict[str, Any],
path: FQNPath = (),
paths: Optional[MutableSet[FQNPath]] = None,
) -> PathSet:
if paths is None:
paths = set()
for key, value in config.items():
if isinstance(value, dict) and not key.startswith('+'):
self._get_v2_config_paths(value, path + (key,), paths)
else:
paths.add(path)
return frozenset(paths)
def get_resource_config_paths(self) -> Dict[str, PathSet]:
"""Return a dictionary with resource type keys whose values are
lists of lists of strings, where each inner list of strings represents
a configured path in the resource.
"""
return {
'models': self._get_config_paths(self.models),
'seeds': self._get_config_paths(self.seeds),
'snapshots': self._get_config_paths(self.snapshots),
'sources': self._get_config_paths(self.sources),
'tests': self._get_config_paths(self.tests),
}
def get_unused_resource_config_paths(
self,
resource_fqns: Mapping[str, PathSet],
disabled: PathSet,
) -> List[FQNPath]:
"""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
used.
"""
disabled_fqns = frozenset(tuple(fqn) for fqn in disabled)
resource_config_paths = self.get_resource_config_paths()
unused_resource_config_paths = []
for resource_type, config_paths in resource_config_paths.items():
used_fqns = resource_fqns.get(resource_type, frozenset())
fqns = used_fqns | disabled_fqns
for config_path in config_paths:
if not _is_config_used(config_path, fqns):
unused_resource_config_paths.append(
(resource_type,) + config_path
)
return unused_resource_config_paths
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
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) -> Mapping[str, 'RuntimeConfig']:
if self.dependencies is None:
all_projects = {self.project_name: self}
internal_packages = get_include_paths(self.credentials.type)
# raise exception if fewer installed packages than in packages.yml
count_packages_specified = len(self.packages.packages) # type: ignore
count_packages_installed = len(tuple(self._get_project_directories()))
if count_packages_specified > count_packages_installed:
raise_compiler_error(
f'dbt found {count_packages_specified} package(s) '
f'specified in packages.yml, but only '
f'{count_packages_installed} package(s) installed '
f'in {self.modules_path}. Run "dbt deps" to '
f'install package dependencies.'
)
project_paths = itertools.chain(
internal_packages,
self._get_project_directories()
)
for project_name, project in self.load_projects(project_paths):
if project_name in all_projects:
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
self.dependencies = all_projects
return self.dependencies
def clear_dependencies(self):
self.dependencies = None
def load_projects(
self, paths: Iterable[Path]
) -> Iterator[Tuple[str, 'RuntimeConfig']]:
for path in paths:
try:
project = self.new_project(str(path))
except DbtProjectError as e:
raise DbtProjectError(
f'Failed to read package: {e}',
result_type='invalid_project',
path=path,
) from e
else:
yield project.project_name, project
def _get_project_directories(self) -> Iterator[Path]:
root = Path(self.project_root) / self.modules_path
if root.exists():
for path in root.iterdir():
if path.is_dir() and not path.name.startswith('__'):
yield path
class UnsetCredentials(Credentials):
def __init__(self):
super().__init__('', '')
@property
def type(self):
return None
@property
def unique_field(self):
return None
def connection_info(self, *args, **kwargs):
return {}
def _connection_keys(self):
return ()
class UnsetConfig(UserConfig):
def __getattribute__(self, name):
if name in {f.name for f in fields(UserConfig)}:
raise AttributeError(
f"'UnsetConfig' object has no attribute {name}"
)
def __post_serialize__(self, dct):
return {}
class UnsetProfile(Profile):
def __init__(self):
self.credentials = UnsetCredentials()
self.config = UnsetConfig()
self.profile_name = ''
self.target_name = ''
self.threads = -1
def to_target_dict(self):
return {}
def __getattribute__(self, name):
if name in {'profile_name', 'target_name', 'threads'}:
raise RuntimeException(
f'Error: disallowed attribute "{name}" - no profile!'
)
return Profile.__getattribute__(self, name)
@dataclass
class UnsetProfileConfig(RuntimeConfig):
"""This class acts a lot _like_ a RuntimeConfig, except if your profile is
missing, any access to profile members results in an exception.
"""
def __post_init__(self):
# instead of futzing with InitVar overrides or rewriting __init__, just
# `del` the attrs we don't want users touching.
del self.profile_name
del self.target_name
# don't call super().__post_init__(), as that calls validate(), and
# this object isn't very valid
def __getattribute__(self, name):
# Override __getattribute__ to check that the attribute isn't 'banned'.
if name in {'profile_name', 'target_name'}:
raise RuntimeException(
f'Error: disallowed attribute "{name}" - no profile!'
)
# avoid every attribute access triggering infinite recursion
return RuntimeConfig.__getattribute__(self, name)
def to_target_dict(self):
# re-override the poisoned profile behavior
return {}
@classmethod
def from_parts(
cls,
project: Project,
profile: Profile,
args: Any,
dependencies: Optional[Mapping[str, 'RuntimeConfig']] = None,
) -> 'RuntimeConfig':
"""Instantiate a RuntimeConfig from its components.
:param profile: Ignored.
:param project: A parsed dbt Project.
:param args: The parsed command-line arguments.
:returns RuntimeConfig: The new configuration.
"""
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, 'vars', '{}'))
return cls(
project_name=project.project_name,
version=project.version,
project_root=project.project_root,
source_paths=project.source_paths,
macro_paths=project.macro_paths,
data_paths=project.data_paths,
test_paths=project.test_paths,
analysis_paths=project.analysis_paths,
docs_paths=project.docs_paths,
asset_paths=project.asset_paths,
target_path=project.target_path,
snapshot_paths=project.snapshot_paths,
clean_targets=project.clean_targets,
log_path=project.log_path,
modules_path=project.modules_path,
quoting=project.quoting, # we never use this anyway.
models=project.models,
on_run_start=project.on_run_start,
on_run_end=project.on_run_end,
dispatch=project.dispatch,
seeds=project.seeds,
snapshots=project.snapshots,
dbt_version=project.dbt_version,
packages=project.packages,
manifest_selectors=project.manifest_selectors,
selectors=project.selectors,
query_comment=project.query_comment,
sources=project.sources,
tests=project.tests,
vars=project.vars,
config_version=project.config_version,
unrendered=project.unrendered,
profile_name='',
target_name='',
config=UnsetConfig(),
threads=getattr(args, 'threads', 1),
credentials=UnsetCredentials(),
args=args,
cli_vars=cli_vars,
dependencies=dependencies,
)
@classmethod
def _get_rendered_profile(
cls,
args: Any,
profile_renderer: ProfileRenderer,
profile_name: Optional[str],
) -> Profile:
try:
profile = Profile.render_from_args(
args, profile_renderer, profile_name
)
except (DbtProjectError, DbtProfileError) as exc:
logger.debug(
'Profile not loaded due to error: {}', exc, exc_info=True
)
logger.info(
'No profile "{}" found, continuing with no target',
profile_name
)
# return the poisoned form
profile = UnsetProfile()
# disable anonymous usage statistics
tracking.disable_tracking()
return profile
@classmethod
def from_args(cls: Type[RuntimeConfig], args: Any) -> 'RuntimeConfig':
"""Given arguments, read in dbt_project.yml from the current directory,
read in packages.yml if it exists, and use them to find the profile to
load.
:param args: The arguments as parsed from the cli.
:raises DbtProjectError: If the project is invalid or missing.
:raises DbtProfileError: If the profile is invalid or missing.
:raises ValidationException: If the cli variables are invalid.
"""
project, profile = cls.collect_parts(args)
if not isinstance(profile, UnsetProfile):
# if it's a real profile, return a real config
cls = RuntimeConfig
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):
if fqns:
for fqn in fqns:
if len(path) <= len(fqn) and fqn[:len(path)] == path:
return True
return False

View File

@@ -0,0 +1,182 @@
from pathlib import Path
from typing import Dict, Any
from dbt.clients.yaml_helper import ( # noqa: F401
yaml, Loader, Dumper, load_yaml_text
)
from dbt.dataclass_schema import ValidationError
from .renderer import SelectorRenderer
from dbt.clients.system import (
load_file_contents,
path_exists,
resolve_path_from_base,
)
from dbt.contracts.selection import SelectorFile
from dbt.exceptions import DbtSelectorsError, RuntimeException
from dbt.graph import parse_from_selectors_definition, SelectionSpec
from dbt.graph.selector_spec import SelectionCriteria
MALFORMED_SELECTOR_ERROR = """\
The selectors.yml file in this project is malformed. Please double check
the contents of this file and fix any errors before retrying.
You can find more information on the syntax for this file here:
https://docs.getdbt.com/docs/package-management
Validator Error:
{error}
"""
class SelectorConfig(Dict[str, SelectionSpec]):
@classmethod
def selectors_from_dict(cls, data: Dict[str, Any]) -> 'SelectorConfig':
try:
SelectorFile.validate(data)
selector_file = SelectorFile.from_dict(data)
selectors = parse_from_selectors_definition(selector_file)
except ValidationError as exc:
yaml_sel_cfg = yaml.dump(exc.instance)
raise DbtSelectorsError(
f"Could not parse selector file data: \n{yaml_sel_cfg}\n"
f"Valid root-level selector definitions: "
f"union, intersection, string, dictionary. No lists. "
f"\nhttps://docs.getdbt.com/reference/node-selection/"
f"yaml-selectors",
result_type='invalid_selector'
) from exc
except RuntimeException as exc:
raise DbtSelectorsError(
f'Could not read selector file data: {exc}',
result_type='invalid_selector',
) from exc
return cls(selectors)
@classmethod
def render_from_dict(
cls,
data: Dict[str, Any],
renderer: SelectorRenderer,
) -> 'SelectorConfig':
try:
rendered = renderer.render_data(data)
except (ValidationError, RuntimeException) as exc:
raise DbtSelectorsError(
f'Could not render selector data: {exc}',
result_type='invalid_selector',
) from exc
return cls.selectors_from_dict(rendered)
@classmethod
def from_path(
cls, path: Path, renderer: SelectorRenderer,
) -> 'SelectorConfig':
try:
data = load_yaml_text(load_file_contents(str(path)))
except (ValidationError, RuntimeException) as exc:
raise DbtSelectorsError(
f'Could not read selector file: {exc}',
result_type='invalid_selector',
path=path,
) from exc
try:
return cls.render_from_dict(data, renderer)
except DbtSelectorsError as exc:
exc.path = path
raise
def selector_data_from_root(project_root: str) -> Dict[str, Any]:
selector_filepath = resolve_path_from_base(
'selectors.yml', project_root
)
if path_exists(selector_filepath):
selectors_dict = load_yaml_text(load_file_contents(selector_filepath))
else:
selectors_dict = None
return selectors_dict
def selector_config_from_data(
selectors_data: Dict[str, Any]
) -> SelectorConfig:
if not selectors_data:
selectors_data = {'selectors': []}
try:
selectors = SelectorConfig.selectors_from_dict(selectors_data)
except ValidationError as e:
raise DbtSelectorsError(
MALFORMED_SELECTOR_ERROR.format(error=str(e.message)),
result_type='invalid_selector',
) from e
return selectors
# These are utilities to clean up the dictionary created from
# selectors.yml by turning the cli-string format entries into
# normalized dictionary entries. It parallels the flow in
# dbt/graph/cli.py. If changes are made there, it might
# be necessary to make changes here. Ideally it would be
# good to combine the two flows into one at some point.
class SelectorDict:
@classmethod
def parse_dict_definition(cls, definition):
key = list(definition)[0]
value = definition[key]
if isinstance(value, list):
new_values = []
for sel_def in value:
new_value = cls.parse_from_definition(sel_def)
new_values.append(new_value)
value = new_values
if key == 'exclude':
definition = {key: value}
elif len(definition) == 1:
definition = {'method': key, 'value': value}
return definition
@classmethod
def parse_a_definition(cls, def_type, definition):
# this definition must be a list
new_dict = {def_type: []}
for sel_def in definition[def_type]:
if isinstance(sel_def, dict):
sel_def = cls.parse_from_definition(sel_def)
new_dict[def_type].append(sel_def)
elif isinstance(sel_def, str):
sel_def = SelectionCriteria.dict_from_single_spec(sel_def)
new_dict[def_type].append(sel_def)
else:
new_dict[def_type].append(sel_def)
return new_dict
@classmethod
def parse_from_definition(cls, definition):
if isinstance(definition, str):
definition = SelectionCriteria.dict_from_single_spec(definition)
elif 'union' in definition:
definition = cls.parse_a_definition('union', definition)
elif 'intersection' in definition:
definition = cls.parse_a_definition('intersection', definition)
elif isinstance(definition, dict):
definition = cls.parse_dict_definition(definition)
return definition
# This is the normal entrypoint of this code. Give it the
# list of selectors generated from the selectors.yml file.
@classmethod
def parse_from_selectors_list(cls, selectors):
selector_dict = {}
for selector in selectors:
sel_name = selector['name']
selector_dict[sel_name] = selector
definition = cls.parse_from_definition(selector['definition'])
selector_dict[sel_name]['definition'] = definition
return selector_dict

23
core/dbt/config/utils.py Normal file
View File

@@ -0,0 +1,23 @@
from typing import Dict, Any
from dbt.clients import yaml_helper
from dbt.exceptions import raise_compiler_error, ValidationException
from dbt.logger import GLOBAL_LOGGER as logger
def parse_cli_vars(var_string: str) -> Dict[str, Any]:
try:
cli_vars = yaml_helper.load_yaml_text(var_string)
var_type = type(cli_vars)
if var_type is dict:
return cli_vars
else:
type_name = var_type.__name__
raise_compiler_error(
"The --vars argument must be a YAML dictionary, but was "
"of type '{}'".format(type_name))
except ValidationException:
logger.error(
"The YAML provided in the --vars argument is not valid.\n"
)
raise

View File

@@ -1,22 +1,25 @@
import json
import os
from typing import (
Any, Dict, NoReturn, Optional
Any, Dict, NoReturn, Optional, Mapping
)
from dbt import flags
from dbt import tracking
from dbt.clients.jinja import undefined_error, get_rendered
from dbt.clients.yaml_helper import ( # noqa: F401
yaml, safe_load, SafeLoader, Loader, Dumper
)
from dbt.contracts.graph.compiled import CompiledResource
from dbt.exceptions import raise_compiler_error, MacroReturn
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.utils import merge
from dbt.version import __version__ as dbt_version
import yaml
# These modules are added to the context. Consider alternative
# approaches which will extend well to potentially many modules
import pytz
import datetime
import re
def get_pytz_module_context() -> Dict[str, Any]:
@@ -41,10 +44,19 @@ def get_datetime_module_context() -> Dict[str, Any]:
}
def get_re_module_context() -> Dict[str, Any]:
context_exports = re.__all__
return {
name: getattr(re, name) for name in context_exports
}
def get_context_modules() -> Dict[str, Dict[str, Any]]:
return {
'pytz': get_pytz_module_context(),
'datetime': get_datetime_module_context(),
're': get_re_module_context(),
}
@@ -98,48 +110,48 @@ class Var:
"supplied to {} = {}"
_VAR_NOTSET = object()
def __init__(self, model, context, overrides):
self.model = model
self.context = context
def __init__(
self,
context: Mapping[str, Any],
cli_vars: Mapping[str, Any],
node: Optional[CompiledResource] = None
) -> None:
self._context: Mapping[str, Any] = context
self._cli_vars: Mapping[str, Any] = cli_vars
self._node: Optional[CompiledResource] = node
self._merged: Mapping[str, Any] = self._generate_merged()
# These are hard-overrides (eg. CLI vars) that should take
# precedence over context-based var definitions
self.overrides = overrides
def _generate_merged(self) -> Mapping[str, Any]:
return self._cli_vars
if model is None:
# during config parsing we have no model and no local vars
self.model_name = '<Configuration>'
local_vars = {}
@property
def node_name(self):
if self._node is not None:
return self._node.name
else:
self.model_name = model.name
local_vars = model.local_vars()
self.local_vars = merge(local_vars, overrides)
def pretty_dict(self, data):
return json.dumps(data, sort_keys=True, indent=4)
return '<Configuration>'
def get_missing_var(self, var_name):
pretty_vars = self.pretty_dict(self.local_vars)
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.model_name, pretty_vars
var_name, self.node_name, pretty_vars
)
raise_compiler_error(msg, self.model)
raise_compiler_error(msg, self._node)
def assert_var_defined(self, var_name, default):
if var_name not in self.local_vars and default is self._VAR_NOTSET:
return self.get_missing_var(var_name)
def has_var(self, var_name: str):
return var_name in self._merged
def get_rendered_var(self, var_name):
raw = self.local_vars[var_name]
raw = self._merged[var_name]
# if bool/int/float/etc are passed in, don't compile anything
if not isinstance(raw, str):
return raw
return get_rendered(raw, self.context)
return get_rendered(raw, self._context)
def __call__(self, var_name, default=_VAR_NOTSET):
if var_name in self.local_vars:
if self.has_var(var_name):
return self.get_rendered_var(var_name)
elif default is not self._VAR_NOTSET:
return default
@@ -161,6 +173,7 @@ class BaseContext(metaclass=ContextMeta):
builtins[key] = value
return builtins
# no dbtClassMixin so this is not an actual override
def to_dict(self):
self._ctx['context'] = self._ctx
builtins = self.generate_builtins()
@@ -170,15 +183,101 @@ class BaseContext(metaclass=ContextMeta):
@contextproperty
def dbt_version(self) -> str:
"""The `dbt_version` variable returns the installed version of dbt that
is currently running. It can be used for debugging or auditing
purposes.
> macros/get_version.sql
{% macro get_version() %}
{% set msg = "The installed version of dbt is: " ~ dbt_version %}
{% do log(msg, info=true) %}
{% endmacro %}
Example output:
$ dbt run-operation get_version
The installed version of dbt is 0.16.0
"""
return dbt_version
@contextproperty
def var(self) -> Var:
return Var(None, self._ctx, self.cli_vars)
"""Variables can be passed from your `dbt_project.yml` file into models
during compilation. These variables are useful for configuring packages
for deployment in multiple environments, or defining values that should
be used across multiple models within a package.
To add a variable to a model, use the `var()` function:
> my_model.sql:
select * from events where event_type = '{{ var("event_type") }}'
If you try to run this model without supplying an `event_type`
variable, you'll receive a compilation error that looks like this:
Encountered an error:
! Compilation error while compiling model package_name.my_model:
! Required var 'event_type' not found in config:
Vars supplied to package_name.my_model = {
}
To supply a variable to a given model, add one or more `vars`
dictionaries to the `models` config in your `dbt_project.yml` file.
These `vars` are in-scope for all models at or below where they are
defined, so place them where they make the most sense. Below are three
different placements of the `vars` dict, all of which will make the
`my_model` model compile.
> dbt_project.yml:
# 1) scoped at the model level
models:
package_name:
my_model:
materialized: view
vars:
event_type: activation
# 2) scoped at the package level
models:
package_name:
vars:
event_type: activation
my_model:
materialized: view
# 3) scoped globally
models:
vars:
event_type: activation
package_name:
my_model:
materialized: view
## Variable default values
The `var()` function takes an optional second argument, `default`. If
this argument is provided, then it will be the default value for the
variable if one is not explicitly defined.
> my_model.sql:
-- Use 'activation' as the event_type if the variable is not
-- defined.
select *
from events
where event_type = '{{ var("event_type", "activation") }}'
"""
return Var(self._ctx, self.cli_vars)
@contextmember
@staticmethod
def env_var(var: str, default: Optional[str] = None) -> str:
"""The env_var() function. Return the environment variable named 'var'.
If there is no such environment variable set, return the default.
If the default is None, raise an exception for an undefined variable.
"""
if var in os.environ:
return os.environ[var]
elif default is not None:
@@ -191,6 +290,7 @@ class BaseContext(metaclass=ContextMeta):
@contextmember
@staticmethod
def debug():
"""Enter a debugger at this line in the compiled jinja code."""
import sys
import ipdb # type: ignore
frame = sys._getframe(3)
@@ -199,12 +299,48 @@ class BaseContext(metaclass=ContextMeta):
@contextmember('return')
@staticmethod
def _return(value: Any) -> NoReturn:
raise MacroReturn(value)
def _return(data: Any) -> NoReturn:
"""The `return` function can be used in macros to return data to the
caller. The type of the data (`dict`, `list`, `int`, etc) will be
preserved through the return call.
:param data: The data to return to the caller
> macros/example.sql:
{% macro get_data() %}
{{ return([1,2,3]) }}
{% endmacro %}
> models/my_model.sql:
select
-- getdata() returns a list!
{% for i in getdata() %}
{{ i }}
{% if not loop.last %},{% endif %}
{% endfor %}
"""
raise MacroReturn(data)
@contextmember
@staticmethod
def fromjson(string: str, default: Any = None) -> Any:
"""The `fromjson` context method can be used to deserialize a json
string into a Python object primitive, eg. a `dict` or `list`.
:param value: The json string to deserialize
:param default: A default value to return if the `string` argument
cannot be deserialized (optional)
Usage:
{% set my_json_str = '{"abc": 123}' %}
{% set my_dict = fromjson(my_json_str) %}
{% do log(my_dict['abc']) %}
"""
try:
return json.loads(string)
except ValueError:
@@ -215,6 +351,21 @@ class BaseContext(metaclass=ContextMeta):
def tojson(
value: Any, default: Any = None, sort_keys: bool = False
) -> Any:
"""The `tojson` context method can be used to serialize a Python
object primitive, eg. a `dict` or `list` to a json string.
:param value: The value serialize to json
:param default: A default value to return if the `value` argument
cannot be serialized
:param sort_keys: If True, sort the keys.
Usage:
{% set my_dict = {"abc": 123} %}
{% set my_json_string = tojson(my_dict) %}
{% do log(my_json_string) %}
"""
try:
return json.dumps(value, sort_keys=sort_keys)
except ValueError:
@@ -223,8 +374,29 @@ class BaseContext(metaclass=ContextMeta):
@contextmember
@staticmethod
def fromyaml(value: str, default: Any = None) -> Any:
"""The fromyaml context method can be used to deserialize a yaml string
into a Python object primitive, eg. a `dict` or `list`.
:param value: The yaml string to deserialize
:param default: A default value to return if the `string` argument
cannot be deserialized (optional)
Usage:
{% set my_yml_str -%}
dogs:
- good
- bad
{%- endset %}
{% set my_dict = fromyaml(my_yml_str) %}
{% do log(my_dict['dogs'], info=true) %}
-- ["good", "bad"]
{% do my_dict['dogs'].pop() }
{% do log(my_dict['dogs'], info=true) %}
-- ["good"]
"""
try:
return yaml.safe_load(value)
return safe_load(value)
except (AttributeError, ValueError, yaml.YAMLError):
return default
@@ -235,6 +407,21 @@ class BaseContext(metaclass=ContextMeta):
def toyaml(
value: Any, default: Optional[str] = None, sort_keys: bool = False
) -> Optional[str]:
"""The `tojson` context method can be used to serialize a Python
object primitive, eg. a `dict` or `list` to a yaml string.
:param value: The value serialize to yaml
:param default: A default value to return if the `value` argument
cannot be serialized
:param sort_keys: If True, sort the keys.
Usage:
{% set my_dict = {"abc": 123} %}
{% set my_yaml_string = toyaml(my_dict) %}
{% do log(my_yaml_string) %}
"""
try:
return yaml.safe_dump(data=value, sort_keys=sort_keys)
except (ValueError, yaml.YAMLError):
@@ -243,6 +430,18 @@ class BaseContext(metaclass=ContextMeta):
@contextmember
@staticmethod
def log(msg: str, info: bool = False) -> str:
"""Logs a line to either the log file or stdout.
:param msg: The message to log
:param info: If `False`, write to the log file. If `True`, write to
both the log file and stdout.
> macros/my_log_macro.sql
{% macro some_macro(arg1, arg2) %}
{{ log("Running some_macro: " ~ arg1 ~ ", " ~ arg2) }}
{% endmacro %}"
"""
if info:
logger.info(msg)
else:
@@ -251,6 +450,27 @@ class BaseContext(metaclass=ContextMeta):
@contextproperty
def run_started_at(self) -> Optional[datetime.datetime]:
"""`run_started_at` outputs the timestamp that this run started, e.g.
`2017-04-21 01:23:45.678`. The `run_started_at` variable is a Python
`datetime` object. As of 0.9.1, the timezone of this variable defaults
to UTC.
> run_started_at_example.sql
select
'{{ run_started_at.strftime("%Y-%m-%d") }}' as date_day
from ...
To modify the timezone of this variable, use the the `pytz` module:
> run_started_at_utc.sql
{% set est = modules.pytz.timezone("America/New_York") %}
select
'{{ run_started_at.astimezone(est) }}' as run_started_est
from ...
"""
if tracking.active_user is not None:
return tracking.active_user.run_started_at
else:
@@ -258,6 +478,9 @@ class BaseContext(metaclass=ContextMeta):
@contextproperty
def invocation_id(self) -> Optional[str]:
"""invocation_id outputs a UUID generated for this dbt run (useful for
auditing)
"""
if tracking.active_user is not None:
return tracking.active_user.invocation_id
else:
@@ -265,13 +488,55 @@ class BaseContext(metaclass=ContextMeta):
@contextproperty
def modules(self) -> Dict[str, Any]:
"""The `modules` variable in the Jinja context contains useful Python
modules for operating on data.
# datetime
This variable is a pointer to the Python datetime module.
Usage:
{% set dt = modules.datetime.datetime.now() %}
# pytz
This variable is a pointer to the Python pytz module.
Usage:
{% set dt = modules.datetime.datetime(2002, 10, 27, 6, 0, 0) %}
{% set dt_local = modules.pytz.timezone('US/Eastern').localize(dt) %}
{{ dt_local }}
""" # noqa
return get_context_modules()
@contextproperty
def flags(self) -> Any:
"""The `flags` variable contains true/false values for flags provided
on the command line.
> flags.sql:
{% if flags.FULL_REFRESH %}
drop table ...
{% else %}
-- no-op
{% endif %}
The list of valid flags are:
- `flags.STRICT_MODE`: True if `--strict` (or `-S`) was provided on the
command line
- `flags.FULL_REFRESH`: True if `--full-refresh` was provided on the
command line
- `flags.NON_DESTRUCTIVE`: True if `--non-destructive` was provided on
the command line
"""
return flags
def generate_base_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]:
ctx = BaseContext(cli_vars)
# This is not a Mashumaro to_dict call
return ctx.to_dict()

View File

@@ -1,15 +1,11 @@
from typing import Any, Dict, Iterable, Union, Optional
from typing import Any, Dict
from dbt.clients.jinja import MacroGenerator, MacroStack
from dbt.contracts.connection import AdapterRequiredConfig
from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.graph.parsed import ParsedMacro
from dbt.include.global_project import PACKAGES
from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
from dbt.node_types import NodeType
from dbt.utils import MultiDict
from dbt.context.base import contextproperty
from dbt.context.base import contextproperty, Var
from dbt.context.target import TargetContext
from dbt.exceptions import raise_duplicate_macro_name
class ConfiguredContext(TargetContext):
@@ -25,112 +21,80 @@ class ConfiguredContext(TargetContext):
return self.config.project_name
FlatNamespace = Dict[str, MacroGenerator]
NamespaceMember = Union[FlatNamespace, MacroGenerator]
FullNamespace = Dict[str, NamespaceMember]
class FQNLookup:
def __init__(self, package_name: str):
self.package_name = package_name
self.fqn = [package_name]
self.resource_type = NodeType.Model
class MacroNamespace:
def __init__(
self,
root_package: str,
search_package: str,
thread_ctx: MacroStack,
node: Optional[Any] = None,
) -> None:
self.root_package = root_package
self.search_package = search_package
self.globals: FlatNamespace = {}
self.locals: FlatNamespace = {}
self.packages: Dict[str, FlatNamespace] = {}
self.thread_ctx = thread_ctx
self.node = node
def add_macro(self, macro: ParsedMacro, ctx: Dict[str, Any]):
macro_name: str = macro.name
macro_func: MacroGenerator = MacroGenerator(
macro, ctx, self.node, self.thread_ctx
)
# put plugin macros into the root namespace
if macro.package_name in PACKAGES:
namespace: str = GLOBAL_PROJECT_NAME
else:
namespace = macro.package_name
if namespace not in self.packages:
value: Dict[str, MacroGenerator] = {}
self.packages[namespace] = value
if macro_name in self.packages[namespace]:
raise_duplicate_macro_name(macro_func.macro, macro, namespace)
self.packages[namespace][macro_name] = macro_func
if namespace == self.search_package:
self.locals[macro_name] = macro_func
elif namespace in {self.root_package, GLOBAL_PROJECT_NAME}:
self.globals[macro_name] = macro_func
def add_macros(self, macros: Iterable[ParsedMacro], ctx: Dict[str, Any]):
for macro in macros:
self.add_macro(macro, ctx)
def get_macro_dict(self) -> FullNamespace:
root_namespace: FullNamespace = {}
root_namespace.update(self.packages)
root_namespace.update(self.globals)
root_namespace.update(self.locals)
return root_namespace
class ManifestContext(ConfiguredContext):
"""The Macro context has everything in the target context, plus the macros
in the manifest.
The given macros can override any previous context values, which will be
available as if they were accessed relative to the package name.
"""
class ConfiguredVar(Var):
def __init__(
self,
context: Dict[str, Any],
config: AdapterRequiredConfig,
manifest: Manifest,
search_package: str,
) -> None:
super().__init__(config)
self.manifest = manifest
self.search_package = search_package
self.macro_stack = MacroStack()
project_name: str,
):
super().__init__(context, config.cli_vars)
self._config = config
self._project_name = project_name
def _get_namespace(self):
return MacroNamespace(
self.config.project_name,
self.search_package,
self.macro_stack,
None,
def __call__(self, var_name, default=Var._VAR_NOTSET):
my_config = self._config.load_dependencies()[self._project_name]
# cli vars > active project > local project
if var_name in self._config.cli_vars:
return self._config.cli_vars[var_name]
adapter_type = self._config.credentials.type
lookup = FQNLookup(self._project_name)
active_vars = self._config.vars.vars_for(lookup, adapter_type)
all_vars = MultiDict([active_vars])
if self._config.project_name != my_config.project_name:
all_vars.add(my_config.vars.vars_for(lookup, adapter_type))
if var_name in all_vars:
return all_vars[var_name]
if default is not Var._VAR_NOTSET:
return default
return self.get_missing_var(var_name)
class SchemaYamlContext(ConfiguredContext):
def __init__(self, config, project_name: str):
super().__init__(config)
self._project_name = project_name
@contextproperty
def var(self) -> ConfiguredVar:
return ConfiguredVar(
self._ctx, self.config, self._project_name
)
def get_macros(self) -> Dict[str, Any]:
nsp = self._get_namespace()
nsp.add_macros(self.manifest.macros.values(), self._ctx)
return nsp.get_macro_dict()
def to_dict(self) -> Dict[str, Any]:
dct = super().to_dict()
dct.update(self.get_macros())
return dct
class MacroResolvingContext(ConfiguredContext):
def __init__(self, config):
super().__init__(config)
@contextproperty
def var(self) -> ConfiguredVar:
return ConfiguredVar(
self._ctx, self.config, self.config.project_name
)
class QueryHeaderContext(ManifestContext):
def __init__(
self, config: AdapterRequiredConfig, manifest: Manifest
) -> None:
super().__init__(config, manifest, config.project_name)
def generate_query_header_context(
config: AdapterRequiredConfig, manifest: Manifest
):
ctx = QueryHeaderContext(config, manifest)
def generate_schema_yml(
config: AdapterRequiredConfig, project_name: str
) -> Dict[str, Any]:
ctx = SchemaYamlContext(config, project_name)
return ctx.to_dict()
def generate_macro_context(
config: AdapterRequiredConfig,
) -> Dict[str, Any]:
ctx = MacroResolvingContext(config)
return ctx.to_dict()

View File

@@ -0,0 +1,313 @@
from abc import abstractmethod
from copy import deepcopy
from dataclasses import dataclass
from typing import List, Iterator, Dict, Any, TypeVar, Generic
from dbt.config import RuntimeConfig, Project, IsFQNResource
from dbt.contracts.graph.model_config import BaseConfig, get_config_for
from dbt.exceptions import InternalException
from dbt.node_types import NodeType
from dbt.utils import fqn_search
@dataclass
class ModelParts(IsFQNResource):
fqn: List[str]
resource_type: NodeType
package_name: str
T = TypeVar('T') # any old type
C = TypeVar('C', bound=BaseConfig)
class ConfigSource:
def __init__(self, project):
self.project = project
def get_config_dict(self, resource_type: NodeType):
...
class UnrenderedConfig(ConfigSource):
def __init__(self, project: Project):
self.project = project
def get_config_dict(self, resource_type: NodeType) -> Dict[str, Any]:
unrendered = self.project.unrendered.project_dict
if resource_type == NodeType.Seed:
model_configs = unrendered.get('seeds')
elif resource_type == NodeType.Snapshot:
model_configs = unrendered.get('snapshots')
elif resource_type == NodeType.Source:
model_configs = unrendered.get('sources')
elif resource_type == NodeType.Test:
model_configs = unrendered.get('tests')
else:
model_configs = unrendered.get('models')
if model_configs is None:
return {}
else:
return model_configs
class RenderedConfig(ConfigSource):
def __init__(self, project: Project):
self.project = project
def get_config_dict(self, resource_type: NodeType) -> Dict[str, Any]:
if resource_type == NodeType.Seed:
model_configs = self.project.seeds
elif resource_type == NodeType.Snapshot:
model_configs = self.project.snapshots
elif resource_type == NodeType.Source:
model_configs = self.project.sources
elif resource_type == NodeType.Test:
model_configs = self.project.tests
else:
model_configs = self.project.models
return model_configs
class BaseContextConfigGenerator(Generic[T]):
def __init__(self, active_project: RuntimeConfig):
self._active_project = active_project
def get_config_source(self, project: Project) -> ConfigSource:
return RenderedConfig(project)
def get_node_project(self, project_name: str):
if project_name == self._active_project.project_name:
return self._active_project
dependencies = self._active_project.load_dependencies()
if project_name not in dependencies:
raise InternalException(
f'Project name {project_name} not found in dependencies '
f'(found {list(dependencies)})'
)
return dependencies[project_name]
def _project_configs(
self, project: Project, fqn: List[str], resource_type: NodeType
) -> Iterator[Dict[str, Any]]:
src = self.get_config_source(project)
model_configs = src.get_config_dict(resource_type)
for level_config in fqn_search(model_configs, fqn):
result = {}
for key, value in level_config.items():
if key.startswith('+'):
result[key[1:].strip()] = deepcopy(value)
elif not isinstance(value, dict):
result[key] = deepcopy(value)
yield result
def _active_project_configs(
self, fqn: List[str], resource_type: NodeType
) -> Iterator[Dict[str, Any]]:
return self._project_configs(self._active_project, fqn, resource_type)
@abstractmethod
def _update_from_config(
self, result: T, partial: Dict[str, Any], validate: bool = False
) -> T:
...
@abstractmethod
def initial_result(self, resource_type: NodeType, base: bool) -> T:
...
def calculate_node_config(
self,
config_call_dict: Dict[str, Any],
fqn: List[str],
resource_type: NodeType,
project_name: str,
base: bool,
patch_config_dict: Dict[str, Any] = None
) -> BaseConfig:
own_config = self.get_node_project(project_name)
result = self.initial_result(resource_type=resource_type, base=base)
project_configs = self._project_configs(own_config, fqn, resource_type)
for fqn_config in project_configs:
result = self._update_from_config(result, fqn_config)
# When schema files patch config, it has lower precedence than
# config in the models (config_call_dict), so we add the patch_config_dict
# before the config_call_dict
if patch_config_dict:
result = self._update_from_config(result, patch_config_dict)
# config_calls are created in the 'experimental' model parser and
# the ParseConfigObject (via add_config_call)
result = self._update_from_config(result, config_call_dict)
if own_config.project_name != self._active_project.project_name:
for fqn_config in self._active_project_configs(fqn, resource_type):
result = self._update_from_config(result, fqn_config)
# this is mostly impactful in the snapshot config case
return result
@abstractmethod
def calculate_node_config_dict(
self,
config_call_dict: Dict[str, Any],
fqn: List[str],
resource_type: NodeType,
project_name: str,
base: bool,
patch_config_dict: Dict[str, Any],
) -> Dict[str, Any]:
...
class ContextConfigGenerator(BaseContextConfigGenerator[C]):
def __init__(self, active_project: RuntimeConfig):
self._active_project = active_project
def get_config_source(self, project: Project) -> ConfigSource:
return RenderedConfig(project)
def initial_result(self, resource_type: NodeType, base: bool) -> C:
# defaults, own_config, config calls, active_config (if != own_config)
config_cls = get_config_for(resource_type, base=base)
# Calculate the defaults. We don't want to validate the defaults,
# because it might be invalid in the case of required config members
# (such as on snapshots!)
result = config_cls.from_dict({})
return result
def _update_from_config(
self, result: C, partial: Dict[str, Any], validate: bool = False
) -> C:
translated = self._active_project.credentials.translate_aliases(
partial
)
return result.update_from(
translated,
self._active_project.credentials.type,
validate=validate
)
def calculate_node_config_dict(
self,
config_call_dict: Dict[str, Any],
fqn: List[str],
resource_type: NodeType,
project_name: str,
base: bool,
patch_config_dict: dict = None
) -> Dict[str, Any]:
config = self.calculate_node_config(
config_call_dict=config_call_dict,
fqn=fqn,
resource_type=resource_type,
project_name=project_name,
base=base,
patch_config_dict=patch_config_dict
)
finalized = config.finalize_and_validate()
return finalized.to_dict(omit_none=True)
class UnrenderedConfigGenerator(BaseContextConfigGenerator[Dict[str, Any]]):
def get_config_source(self, project: Project) -> ConfigSource:
return UnrenderedConfig(project)
def calculate_node_config_dict(
self,
config_call_dict: Dict[str, Any],
fqn: List[str],
resource_type: NodeType,
project_name: str,
base: bool,
patch_config_dict: dict = None
) -> Dict[str, Any]:
return self.calculate_node_config(
config_call_dict=config_call_dict,
fqn=fqn,
resource_type=resource_type,
project_name=project_name,
base=base,
patch_config_dict=patch_config_dict
)
def initial_result(
self,
resource_type: NodeType,
base: bool
) -> Dict[str, Any]:
return {}
def _update_from_config(
self,
result: Dict[str, Any],
partial: Dict[str, Any],
validate: bool = False,
) -> Dict[str, Any]:
translated = self._active_project.credentials.translate_aliases(
partial
)
result.update(translated)
return result
class ContextConfig:
def __init__(
self,
active_project: RuntimeConfig,
fqn: List[str],
resource_type: NodeType,
project_name: str,
) -> None:
self._config_call_dict: Dict[str, Any] = {}
self._active_project = active_project
self._fqn = fqn
self._resource_type = resource_type
self._project_name = project_name
def add_config_call(self, opts: Dict[str, Any]) -> None:
dct = self._config_call_dict
self._add_config_call(dct, opts)
@classmethod
def _add_config_call(cls, config_call_dict, opts: Dict[str, Any]) -> None:
for k, v in opts.items():
# MergeBehavior for post-hook and pre-hook is to collect all
# values, instead of overwriting
if k in BaseConfig.mergebehavior['append']:
if not isinstance(v, list):
v = [v]
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)
elif k in config_call_dict and isinstance(config_call_dict[k], dict):
config_call_dict[k].update(v)
else:
config_call_dict[k] = v
def build_config_dict(
self,
base: bool = False,
*,
rendered: bool = True,
patch_config_dict: dict = None
) -> Dict[str, Any]:
if rendered:
src = ContextConfigGenerator(self._active_project)
else:
src = UnrenderedConfigGenerator(self._active_project)
return src.calculate_node_config_dict(
config_call_dict=self._config_call_dict,
fqn=self._fqn,
resource_type=self._resource_type,
project_name=self._project_name,
base=base,
patch_config_dict=patch_config_dict
)

View File

@@ -12,28 +12,10 @@ from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.graph.parsed import ParsedMacro
from dbt.context.base import contextmember
from dbt.context.configured import ConfiguredContext
from dbt.context.configured import SchemaYamlContext
class DocsParseContext(ConfiguredContext):
def __init__(
self,
config: RuntimeConfig,
node: Any,
) -> None:
super().__init__(config)
self.node = node
@contextmember
def doc(self, *args: str) -> str:
# when you call doc(), this is what happens at parse time
if len(args) != 1 and len(args) != 2:
doc_invalid_args(self.node, args)
# At parse time, nothing should care about what doc() returns
return ''
class DocsRuntimeContext(ConfiguredContext):
class DocsRuntimeContext(SchemaYamlContext):
def __init__(
self,
config: RuntimeConfig,
@@ -41,13 +23,31 @@ class DocsRuntimeContext(ConfiguredContext):
manifest: Manifest,
current_project: str,
) -> None:
super().__init__(config)
super().__init__(config, current_project)
self.node = node
self.manifest = manifest
self.current_project = current_project
@contextmember
def doc(self, *args: str) -> str:
"""The `doc` function is used to reference docs blocks in schema.yml
files. It is analogous to the `ref` function. For more information,
consult the Documentation guide.
> orders.md:
{% docs orders %}
# docs
- go
- here
{% enddocs %}
> schema.yml
version: 2
models:
- name: orders
description: "{{ doc('orders') }}"
"""
# when you call doc(), this is what happens at runtime
if len(args) == 1:
doc_package_name = None
@@ -57,27 +57,24 @@ class DocsRuntimeContext(ConfiguredContext):
else:
doc_invalid_args(self.node, args)
# ParsedDocumentation
target_doc = self.manifest.resolve_doc(
doc_name,
doc_package_name,
self.current_project,
self._project_name,
self.node.package_name,
)
if target_doc is None:
if target_doc:
file_id = target_doc.file_id
if file_id in self.manifest.files:
source_file = self.manifest.files[file_id]
source_file.add_node(self.node.unique_id)
else:
doc_target_not_found(self.node, doc_name, doc_package_name)
return target_doc.block_contents
def generate_parser_docs(
config: RuntimeConfig,
unparsed: Any,
) -> Dict[str, Any]:
ctx = DocsParseContext(config, unparsed)
return ctx.to_dict()
def generate_runtime_docs(
config: RuntimeConfig,
target: Any,
@@ -85,4 +82,5 @@ def generate_runtime_docs(
current_project: str,
) -> Dict[str, Any]:
ctx = DocsRuntimeContext(config, target, manifest, current_project)
# This is not a Mashumaro to_dict call
return ctx.to_dict()

View File

@@ -0,0 +1,199 @@
from typing import (
Dict, MutableMapping, Optional
)
from dbt.contracts.graph.parsed import ParsedMacro
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.clients.jinja import MacroGenerator
MacroNamespace = Dict[str, ParsedMacro]
# This class builds the MacroResolver by adding macros
# to various categories for finding macros in the right order,
# so that higher precedence macros are found first.
# This functionality is also provided by the MacroNamespace,
# but the intention is to eventually replace that class.
# This enables us to get the macro unique_id without
# processing every macro in the project.
# Note: the root project macros override everything in the
# dbt internal projects. External projects (dependencies) will
# use their own macros first, then pull from the root project
# followed by dbt internal projects.
class MacroResolver:
def __init__(
self,
macros: MutableMapping[str, ParsedMacro],
root_project_name: str,
internal_package_names,
) -> None:
self.root_project_name = root_project_name
self.macros = macros
# internal packages comes from get_adapter_package_names
self.internal_package_names = internal_package_names
# To be filled in from macros.
self.internal_packages: Dict[str, MacroNamespace] = {}
self.packages: Dict[str, MacroNamespace] = {}
self.root_package_macros: MacroNamespace = {}
# add the macros to internal_packages, packages, and root packages
self.add_macros()
self._build_internal_packages_namespace()
self._build_macros_by_name()
def _build_internal_packages_namespace(self):
# Iterate in reverse-order and overwrite: the packages that are first
# in the list are the ones we want to "win".
self.internal_packages_namespace: MacroNamespace = {}
for pkg in reversed(self.internal_package_names):
if pkg in self.internal_packages:
# Turn the internal packages into a flat namespace
self.internal_packages_namespace.update(
self.internal_packages[pkg])
# search order:
# local_namespace (package of particular node), not including
# the internal packages or the root package
# This means that within an extra package, it uses its own macros
# root package namespace
# non-internal packages (that aren't local or root)
# dbt internal packages
def _build_macros_by_name(self):
macros_by_name = {}
# all internal packages (already in the right order)
for macro in self.internal_packages_namespace.values():
macros_by_name[macro.name] = macro
# non-internal packages
for fnamespace in self.packages.values():
for macro in fnamespace.values():
macros_by_name[macro.name] = macro
# root package macros
for macro in self.root_package_macros.values():
macros_by_name[macro.name] = macro
self.macros_by_name = macros_by_name
def _add_macro_to(
self,
package_namespaces: Dict[str, MacroNamespace],
macro: ParsedMacro,
):
if macro.package_name in package_namespaces:
namespace = package_namespaces[macro.package_name]
else:
namespace = {}
package_namespaces[macro.package_name] = namespace
if macro.name in namespace:
raise_duplicate_macro_name(
macro, macro, macro.package_name
)
package_namespaces[macro.package_name][macro.name] = macro
def add_macro(self, macro: ParsedMacro):
macro_name: str = macro.name
# internal macros (from plugins) will be processed separately from
# project macros, so store them in a different place
if macro.package_name in self.internal_package_names:
self._add_macro_to(self.internal_packages, macro)
else:
# if it's not an internal package
self._add_macro_to(self.packages, macro)
# add to root_package_macros if it's in the root package
if macro.package_name == self.root_project_name:
self.root_package_macros[macro_name] = macro
def add_macros(self):
for macro in self.macros.values():
self.add_macro(macro)
def get_macro(self, local_package, macro_name):
local_package_macros = {}
if (local_package not in self.internal_package_names and
local_package in self.packages):
local_package_macros = self.packages[local_package]
# First: search the local packages for this macro
if macro_name in local_package_macros:
return local_package_macros[macro_name]
# Now look up in the standard search order
if macro_name in self.macros_by_name:
return self.macros_by_name[macro_name]
return None
def get_macro_id(self, local_package, macro_name):
macro = self.get_macro(local_package, macro_name)
if macro is None:
return None
else:
return macro.unique_id
# Currently this is just used by test processing in the schema
# parser (in connection with the MacroResolver). Future work
# will extend the use of these classes to other parsing areas.
# One of the features of this class compared to the MacroNamespace
# is that you can limit the number of macros provided to the
# context dictionary in the 'to_dict' manifest method.
class TestMacroNamespace:
def __init__(
self, macro_resolver, ctx, node, thread_ctx, depends_on_macros
):
self.macro_resolver = macro_resolver
self.ctx = ctx
self.node = node # can be none
self.thread_ctx = thread_ctx
self.local_namespace = {}
self.project_namespace = {}
if depends_on_macros:
dep_macros = []
self.recursively_get_depends_on_macros(depends_on_macros, dep_macros)
for macro_unique_id in dep_macros:
if macro_unique_id in self.macro_resolver.macros:
# Split up the macro unique_id to get the project_name
(_, project_name, macro_name) = macro_unique_id.split('.')
# Save the plain macro_name in the local_namespace
macro = self.macro_resolver.macros[macro_unique_id]
macro_gen = MacroGenerator(
macro, self.ctx, self.node, self.thread_ctx,
)
self.local_namespace[macro_name] = macro_gen
# We also need the two part macro name
if project_name not in self.project_namespace:
self.project_namespace[project_name] = {}
self.project_namespace[project_name][macro_name] = macro_gen
def recursively_get_depends_on_macros(self, depends_on_macros, dep_macros):
for macro_unique_id in depends_on_macros:
if macro_unique_id in dep_macros:
continue
dep_macros.append(macro_unique_id)
if macro_unique_id in self.macro_resolver.macros:
macro = self.macro_resolver.macros[macro_unique_id]
if macro.depends_on.macros:
self.recursively_get_depends_on_macros(macro.depends_on.macros, dep_macros)
def get_from_package(
self, package_name: Optional[str], name: str
) -> Optional[MacroGenerator]:
macro = None
if package_name is None:
macro = self.macro_resolver.macros_by_name.get(name)
elif package_name == GLOBAL_PROJECT_NAME:
macro = self.macro_resolver.internal_packages_namespace.get(name)
elif package_name in self.macro_resolver.packages:
macro = self.macro_resolver.packages[package_name].get(name)
else:
raise_compiler_error(
f"Could not find package '{package_name}'"
)
if not macro:
return None
macro_func = MacroGenerator(
macro, self.ctx, self.node, self.thread_ctx
)
return macro_func

182
core/dbt/context/macros.py Normal file
View File

@@ -0,0 +1,182 @@
from typing import (
Any, Dict, Iterable, Union, Optional, List, Iterator, Mapping, Set
)
from dbt.clients.jinja import MacroGenerator, MacroStack
from dbt.contracts.graph.parsed import ParsedMacro
from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
from dbt.exceptions import (
raise_duplicate_macro_name, raise_compiler_error
)
FlatNamespace = Dict[str, MacroGenerator]
NamespaceMember = Union[FlatNamespace, MacroGenerator]
FullNamespace = Dict[str, NamespaceMember]
# The point of this class is to collect the various macros
# and provide the ability to flatten them into the ManifestContexts
# that are created for jinja, so that macro calls can be resolved.
# Creates special iterators and _keys methods to flatten the lists.
# When this class is created it has a static 'local_namespace' which
# depends on the package of the node, so it only works for one
# particular local package at a time for "flattening" into a context.
# 'get_by_package' should work for any macro.
class MacroNamespace(Mapping):
def __init__(
self,
global_namespace: FlatNamespace, # root package macros
local_namespace: FlatNamespace, # packages for *this* node
global_project_namespace: FlatNamespace, # internal packages
packages: Dict[str, FlatNamespace], # non-internal packages
):
self.global_namespace: FlatNamespace = global_namespace
self.local_namespace: FlatNamespace = local_namespace
self.packages: Dict[str, FlatNamespace] = packages
self.global_project_namespace: FlatNamespace = global_project_namespace
def _search_order(self) -> Iterable[Union[FullNamespace, FlatNamespace]]:
yield self.local_namespace # local package
yield self.global_namespace # root package
yield self.packages # non-internal packages
yield {
GLOBAL_PROJECT_NAME: self.global_project_namespace, # dbt
}
yield self.global_project_namespace # other internal project besides dbt
# provides special keys method for MacroNamespace iterator
# returns keys from local_namespace, global_namespace, packages,
# global_project_namespace
def _keys(self) -> Set[str]:
keys: Set[str] = set()
for search in self._search_order():
keys.update(search)
return keys
# special iterator using special keys
def __iter__(self) -> Iterator[str]:
for key in self._keys():
yield key
def __len__(self):
return len(self._keys())
def __getitem__(self, key: str) -> NamespaceMember:
for dct in self._search_order():
if key in dct:
return dct[key]
raise KeyError(key)
def get_from_package(
self, package_name: Optional[str], name: str
) -> Optional[MacroGenerator]:
pkg: FlatNamespace
if package_name is None:
return self.get(name)
elif package_name == GLOBAL_PROJECT_NAME:
return self.global_project_namespace.get(name)
elif package_name in self.packages:
return self.packages[package_name].get(name)
else:
raise_compiler_error(
f"Could not find package '{package_name}'"
)
# This class builds the MacroNamespace by adding macros to
# internal_packages or packages, and locals/globals.
# Call 'build_namespace' to return a MacroNamespace.
# This is used by ManifestContext (and subclasses)
class MacroNamespaceBuilder:
def __init__(
self,
root_package: str,
search_package: str,
thread_ctx: MacroStack,
internal_packages: List[str],
node: Optional[Any] = None,
) -> None:
self.root_package = root_package
self.search_package = search_package
# internal packages comes from get_adapter_package_names
self.internal_package_names = set(internal_packages)
self.internal_package_names_order = internal_packages
# macro_func is added here if in root package, since
# the root package acts as a "global" namespace, overriding
# everything else except local external package macro calls
self.globals: FlatNamespace = {}
# macro_func is added here if it's the package for this node
self.locals: FlatNamespace = {}
# Create a dictionary of [package name][macro name] =
# MacroGenerator object which acts like a function
self.internal_packages: Dict[str, FlatNamespace] = {}
self.packages: Dict[str, FlatNamespace] = {}
self.thread_ctx = thread_ctx
self.node = node
def _add_macro_to(
self,
hierarchy: Dict[str, FlatNamespace],
macro: ParsedMacro,
macro_func: MacroGenerator,
):
if macro.package_name in hierarchy:
namespace = hierarchy[macro.package_name]
else:
namespace = {}
hierarchy[macro.package_name] = namespace
if macro.name in namespace:
raise_duplicate_macro_name(
macro_func.macro, macro, macro.package_name
)
hierarchy[macro.package_name][macro.name] = macro_func
def add_macro(self, macro: ParsedMacro, ctx: Dict[str, Any]):
macro_name: str = macro.name
# MacroGenerator is in clients/jinja.py
# a MacroGenerator object is a callable object that will
# execute the MacroGenerator.__call__ function
macro_func: MacroGenerator = MacroGenerator(
macro, ctx, self.node, self.thread_ctx
)
# internal macros (from plugins) will be processed separately from
# project macros, so store them in a different place
if macro.package_name in self.internal_package_names:
self._add_macro_to(self.internal_packages, macro, macro_func)
else:
# if it's not an internal package
self._add_macro_to(self.packages, macro, macro_func)
# add to locals if it's the package this node is in
if macro.package_name == self.search_package:
self.locals[macro_name] = macro_func
# add to globals if it's in the root package
elif macro.package_name == self.root_package:
self.globals[macro_name] = macro_func
def add_macros(self, macros: Iterable[ParsedMacro], ctx: Dict[str, Any]):
for macro in macros:
self.add_macro(macro, ctx)
def build_namespace(
self, macros: Iterable[ParsedMacro], ctx: Dict[str, Any]
) -> MacroNamespace:
self.add_macros(macros, ctx)
# Iterate in reverse-order and overwrite: the packages that are first
# in the list are the ones we want to "win".
global_project_namespace: FlatNamespace = {}
for pkg in reversed(self.internal_package_names_order):
if pkg in self.internal_packages:
# add the macros pointed to by this package name
global_project_namespace.update(self.internal_packages[pkg])
return MacroNamespace(
global_namespace=self.globals, # root package macros
local_namespace=self.locals, # packages for *this* node
global_project_namespace=global_project_namespace, # internal packages
packages=self.packages, # non internal_packages
)

View File

@@ -0,0 +1,82 @@
from typing import List
from dbt.clients.jinja import MacroStack
from dbt.contracts.connection import AdapterRequiredConfig
from dbt.contracts.graph.manifest import Manifest
from dbt.context.macro_resolver import TestMacroNamespace
from .configured import ConfiguredContext
from .macros import MacroNamespaceBuilder
class ManifestContext(ConfiguredContext):
"""The Macro context has everything in the target context, plus the macros
in the manifest.
The given macros can override any previous context values, which will be
available as if they were accessed relative to the package name.
"""
def __init__(
self,
config: AdapterRequiredConfig,
manifest: Manifest,
search_package: str,
) -> None:
super().__init__(config)
self.manifest = manifest
# this is the package of the node for which this context was built
self.search_package = search_package
self.macro_stack = MacroStack()
# This namespace is used by the BaseDatabaseWrapper in jinja rendering.
# The namespace is passed to it when it's constructed. It expects
# to be able to do: namespace.get_from_package(..)
self.namespace = self._build_namespace()
def _build_namespace(self):
# this takes all the macros in the manifest and adds them
# to the MacroNamespaceBuilder stored in self.namespace
builder = self._get_namespace_builder()
return builder.build_namespace(
self.manifest.macros.values(), self._ctx
)
def _get_namespace_builder(self) -> MacroNamespaceBuilder:
# avoid an import loop
from dbt.adapters.factory import get_adapter_package_names
internal_packages: List[str] = get_adapter_package_names(
self.config.credentials.type
)
return MacroNamespaceBuilder(
self.config.project_name,
self.search_package,
self.macro_stack,
internal_packages,
None,
)
# This does not use the Mashumaro code
def to_dict(self):
dct = super().to_dict()
# This moves all of the macros in the 'namespace' into top level
# keys in the manifest dictionary
if isinstance(self.namespace, TestMacroNamespace):
dct.update(self.namespace.local_namespace)
dct.update(self.namespace.project_namespace)
else:
dct.update(self.namespace)
return dct
class QueryHeaderContext(ManifestContext):
def __init__(
self, config: AdapterRequiredConfig, manifest: Manifest
) -> None:
super().__init__(config, manifest, config.project_name)
def generate_query_header_context(
config: AdapterRequiredConfig, manifest: Manifest
):
ctx = QueryHeaderContext(config, manifest)
return ctx.to_dict()

File diff suppressed because it is too large Load Diff

View File

@@ -14,19 +14,67 @@ class TargetContext(BaseContext):
@contextproperty
def target(self) -> Dict[str, Any]:
target = dict(
self.config.credentials.connection_info(with_aliases=True)
)
target.update({
'type': self.config.credentials.type,
'threads': self.config.threads,
'name': self.config.target_name,
# not specified, but present for compatibility
'target_name': self.config.target_name,
'profile_name': self.config.profile_name,
'config': self.config.config.to_dict(),
})
return target
"""`target` contains information about your connection to the warehouse
(specified in profiles.yml). Some configs are shared between all
adapters, while others are adapter-specific.
Common:
|----------|-----------|------------------------------------------|
| Variable | Example | Description |
|----------|-----------|------------------------------------------|
| name | dev | Name of the active target |
|----------|-----------|------------------------------------------|
| schema | dbt_alice | Name of the dbt schema (or, dataset on |
| | | BigQuery) |
|----------|-----------|------------------------------------------|
| type | postgres | The active adapter being used. |
|----------|-----------|------------------------------------------|
| threads | 4 | The number of threads in use by dbt |
|----------|-----------|------------------------------------------|
Snowflake:
|----------|-----------|------------------------------------------|
| Variable | Example | Description |
|----------|-----------|------------------------------------------|
| database | RAW | The active target's database. |
|----------|-----------|------------------------------------------|
| warehouse| TRANSFORM | The active target's warehouse. |
|----------|-----------|------------------------------------------|
| user | USERNAME | The active target's user |
|----------|-----------|------------------------------------------|
| role | ROLENAME | The active target's role |
|----------|-----------|------------------------------------------|
| account | abc123 | The active target's account |
|----------|-----------|------------------------------------------|
Postgres/Redshift:
|----------|-------------------|----------------------------------|
| Variable | Example | Description |
|----------|-------------------|----------------------------------|
| dbname | analytics | The active target's database. |
|----------|-------------------|----------------------------------|
| host | abc123.us-west-2. | The active target's host. |
| | redshift.amazonaws| |
| | .com | |
|----------|-------------------|----------------------------------|
| user | dbt_user | The active target's user |
|----------|-------------------|----------------------------------|
| port | 5439 | The active target's port |
|----------|-------------------|----------------------------------|
BigQuery:
|----------|-----------|------------------------------------------|
| Variable | Example | Description |
|----------|-----------|------------------------------------------|
| project | abc-123 | The active target's project. |
|----------|-----------|------------------------------------------|
"""
return self.config.to_target_dict()
def generate_target_context(

View File

@@ -1,11 +0,0 @@
def named_property(name, doc=None):
def get_prop(self):
return self._contents.get(name)
def set_prop(self, value):
self._contents[name] = value
self.validate()
return property(get_prop, set_prop, doc=doc)

View File

@@ -1,27 +1,39 @@
import abc
import itertools
import hashlib
from dataclasses import dataclass, field
from typing import (
Any, ClassVar, Dict, Tuple, Iterable, Optional, NewType, List, Callable,
Union
Any, ClassVar, Dict, Tuple, Iterable, Optional, List, Callable,
)
from typing_extensions import Protocol
from hologram import JsonSchemaMixin
from hologram.helpers import (
StrEnum, register_pattern, ExtensibleJsonSchemaMixin
)
from dbt.contracts.util import Replaceable
from dbt.exceptions import InternalException
from dbt.helper_types import NoValue
from dbt.utils import translate_aliases
from dbt.logger import GLOBAL_LOGGER as logger
from typing_extensions import Protocol
from dbt.dataclass_schema import (
dbtClassMixin, StrEnum, ExtensibleDbtClassMixin, HyphenatedDbtClassMixin,
ValidatedStringMixin, register_pattern
)
from dbt.contracts.util import Replaceable
Identifier = NewType('Identifier', str)
class Identifier(ValidatedStringMixin):
ValidationRegex = r'^[A-Za-z_][A-Za-z0-9_]+$'
# we need register_pattern for jsonschema validation
register_pattern(Identifier, r'^[A-Za-z_][A-Za-z0-9_]+$')
@dataclass
class AdapterResponse(dbtClassMixin):
_message: str
code: Optional[str] = None
rows_affected: Optional[int] = None
def __str__(self):
return self._message
class ConnectionState(StrEnum):
INIT = 'init'
OPEN = 'open'
@@ -30,20 +42,19 @@ class ConnectionState(StrEnum):
@dataclass(init=False)
class Connection(ExtensibleJsonSchemaMixin, Replaceable):
class Connection(ExtensibleDbtClassMixin, Replaceable):
type: Identifier
name: Optional[str]
name: Optional[str] = None
state: ConnectionState = ConnectionState.INIT
transaction_open: bool = False
# prevent serialization
_handle: Optional[Any] = None
_credentials: JsonSchemaMixin = field(init=False)
_credentials: Optional[Any] = None
def __init__(
self,
type: Identifier,
name: Optional[str],
credentials: JsonSchemaMixin,
credentials: dbtClassMixin,
state: ConnectionState = ConnectionState.INIT,
transaction_open: bool = False,
handle: Optional[Any] = None,
@@ -85,10 +96,15 @@ class LazyHandle:
"""Opener must be a callable that takes a Connection object and opens the
connection, updating the handle on the Connection.
"""
def __init__(self, opener: Callable[[Connection], Connection]):
self.opener = opener
def resolve(self, connection: Connection) -> Connection:
logger.debug(
'Opening a new connection, currently in state {}'
.format(connection.state)
)
return self.opener(connection)
@@ -98,7 +114,7 @@ class LazyHandle:
# will work.
@dataclass # type: ignore
class Credentials(
ExtensibleJsonSchemaMixin,
ExtensibleDbtClassMixin,
Replaceable,
metaclass=abc.ABCMeta
):
@@ -112,12 +128,21 @@ class Credentials(
'type not implemented for base credentials class'
)
@abc.abstractproperty
def unique_field(self) -> str:
raise NotImplementedError(
'type not implemented for base credentials class'
)
def hashed_unique_field(self) -> str:
return hashlib.md5(self.unique_field.encode('utf-8')).hexdigest()
def connection_info(
self, *, with_aliases: bool = False
) -> Iterable[Tuple[str, Any]]:
"""Return an ordered iterator of key/value pairs for pretty-printing.
"""
as_dict = self.to_dict(omit_none=False, with_aliases=with_aliases)
as_dict = self.to_dict(omit_none=False)
connection_keys = set(self._connection_keys())
aliases: List[str] = []
if with_aliases:
@@ -133,39 +158,37 @@ class Credentials(
raise NotImplementedError
@classmethod
def from_dict(cls, data):
def __pre_deserialize__(cls, data):
data = super().__pre_deserialize__(data)
data = cls.translate_aliases(data)
return super().from_dict(data)
return data
@classmethod
def translate_aliases(cls, kwargs: Dict[str, Any]) -> Dict[str, Any]:
return translate_aliases(kwargs, cls._ALIASES)
def translate_aliases(
cls, kwargs: Dict[str, Any], recurse: bool = False
) -> Dict[str, Any]:
return translate_aliases(kwargs, cls._ALIASES, recurse)
def to_dict(self, omit_none=True, validate=False, *, with_aliases=False):
serialized = super().to_dict(omit_none=omit_none, validate=validate)
if with_aliases:
serialized.update({
new_name: serialized[canonical_name]
def __post_serialize__(self, dct):
# no super() -- do we need it?
if self._ALIASES:
dct.update({
new_name: dct[canonical_name]
for new_name, canonical_name in self._ALIASES.items()
if canonical_name in serialized
if canonical_name in dct
})
return serialized
return dct
class UserConfigContract(Protocol):
send_anonymous_usage_stats: bool
use_colors: bool
partial_parse: Optional[bool]
printer_width: Optional[int]
use_colors: Optional[bool] = None
partial_parse: Optional[bool] = None
printer_width: Optional[int] = None
def set_values(self, cookie_dir: str) -> None:
...
def to_dict(
self, omit_none: bool = True, validate: bool = False
) -> Dict[str, Any]:
...
class HasCredentials(Protocol):
credentials: Credentials
@@ -174,9 +197,39 @@ class HasCredentials(Protocol):
target_name: str
threads: int
def to_target_dict(self):
raise NotImplementedError('to_target_dict not implemented')
DEFAULT_QUERY_COMMENT = '''
{%- set comment_dict = {} -%}
{%- do comment_dict.update(
app='dbt',
dbt_version=dbt_version,
profile_name=target.get('profile_name'),
target_name=target.get('target_name'),
) -%}
{%- if node is not none -%}
{%- do comment_dict.update(
node_id=node.unique_id,
) -%}
{% else %}
{# in the node context, the connection name is the node_id #}
{%- do comment_dict.update(connection_name=connection_name) -%}
{%- endif -%}
{{ return(tojson(comment_dict)) }}
'''
@dataclass
class QueryComment(HyphenatedDbtClassMixin):
comment: str = DEFAULT_QUERY_COMMENT
append: bool = False
job_label: bool = False
class AdapterRequiredConfig(HasCredentials, Protocol):
project_name: str
query_comment: Optional[Union[str, NoValue]]
query_comment: QueryComment
cli_vars: Dict[str, Any]
target_path: str

298
core/dbt/contracts/files.py Normal file
View File

@@ -0,0 +1,298 @@
import hashlib
import os
from dataclasses import dataclass, field
from mashumaro.types import SerializableType
from typing import List, Optional, Union, Dict, Any
from dbt.dataclass_schema import dbtClassMixin, StrEnum
from .util import SourceKey
MAXIMUM_SEED_SIZE = 1 * 1024 * 1024
MAXIMUM_SEED_SIZE_NAME = '1MB'
class ParseFileType(StrEnum):
Macro = 'macro'
Model = 'model'
Snapshot = 'snapshot'
Analysis = 'analysis'
Test = 'test'
Seed = 'seed'
Documentation = 'docs'
Schema = 'schema'
Hook = 'hook' # not a real filetype, from dbt_project.yml
parse_file_type_to_parser = {
ParseFileType.Macro: 'MacroParser',
ParseFileType.Model: 'ModelParser',
ParseFileType.Snapshot: 'SnapshotParser',
ParseFileType.Analysis: 'AnalysisParser',
ParseFileType.Test: 'DataTestParser',
ParseFileType.Seed: 'SeedParser',
ParseFileType.Documentation: 'DocumentationParser',
ParseFileType.Schema: 'SchemaParser',
ParseFileType.Hook: 'HookParser',
}
@dataclass
class FilePath(dbtClassMixin):
searched_path: str
relative_path: str
project_root: str
@property
def search_key(self) -> str:
# TODO: should this be project name + path relative to project root?
return self.absolute_path
@property
def full_path(self) -> str:
# useful for symlink preservation
return os.path.join(
self.project_root, self.searched_path, self.relative_path
)
@property
def absolute_path(self) -> str:
return os.path.abspath(self.full_path)
@property
def original_file_path(self) -> str:
# this is mostly used for reporting errors. It doesn't show the project
# name, should it?
return os.path.join(
self.searched_path, self.relative_path
)
def seed_too_large(self) -> bool:
"""Return whether the file this represents is over the seed size limit
"""
return os.stat(self.full_path).st_size > MAXIMUM_SEED_SIZE
@dataclass
class FileHash(dbtClassMixin):
name: str # the hash type name
checksum: str # the hashlib.hash_type().hexdigest() of the file contents
@classmethod
def empty(cls):
return FileHash(name='none', checksum='')
@classmethod
def path(cls, path: str):
return FileHash(name='path', checksum=path)
def __eq__(self, other):
if not isinstance(other, FileHash):
return NotImplemented
if self.name == 'none' or self.name != other.name:
return False
return self.checksum == other.checksum
def compare(self, contents: str) -> bool:
"""Compare the file contents with the given hash"""
if self.name == 'none':
return False
return self.from_contents(contents, name=self.name) == self.checksum
@classmethod
def from_contents(cls, contents: str, name='sha256') -> 'FileHash':
"""Create a file hash from the given file contents. The hash is always
the utf-8 encoding of the contents given, because dbt only reads files
as utf-8.
"""
data = contents.encode('utf-8')
checksum = hashlib.new(name, data).hexdigest()
return cls(name=name, checksum=checksum)
@dataclass
class RemoteFile(dbtClassMixin):
@property
def searched_path(self) -> str:
return 'from remote system'
@property
def relative_path(self) -> str:
return 'from remote system'
@property
def absolute_path(self) -> str:
return 'from remote system'
@property
def original_file_path(self):
return 'from remote system'
@dataclass
class BaseSourceFile(dbtClassMixin, SerializableType):
"""Define a source file in dbt"""
path: Union[FilePath, RemoteFile] # the path information
checksum: FileHash
# Seems like knowing which project the file came from would be useful
project_name: Optional[str] = None
# Parse file type: i.e. which parser will process this file
parse_file_type: Optional[ParseFileType] = None
# we don't want to serialize this
contents: Optional[str] = None
# the unique IDs contained in this file
@property
def file_id(self):
if isinstance(self.path, RemoteFile):
return None
if self.checksum.name == 'none':
return None
return f'{self.project_name}://{self.path.original_file_path}'
def _serialize(self):
dct = self.to_dict()
return dct
@classmethod
def _deserialize(cls, dct: Dict[str, int]):
if dct['parse_file_type'] == 'schema':
sf = SchemaSourceFile.from_dict(dct)
else:
sf = SourceFile.from_dict(dct)
return sf
def __post_serialize__(self, dct):
dct = super().__post_serialize__(dct)
# remove empty lists to save space
dct_keys = list(dct.keys())
for key in dct_keys:
if isinstance(dct[key], list) and not dct[key]:
del dct[key]
# remove contents. Schema files will still have 'dict_from_yaml'
# from the contents
if 'contents' in dct:
del dct['contents']
return dct
@dataclass
class SourceFile(BaseSourceFile):
nodes: List[str] = field(default_factory=list)
docs: List[str] = field(default_factory=list)
macros: List[str] = field(default_factory=list)
@classmethod
def big_seed(cls, path: FilePath) -> 'SourceFile':
"""Parse seeds over the size limit with just the path"""
self = cls(path=path, checksum=FileHash.path(path.original_file_path))
self.contents = ''
return self
def add_node(self, value):
if value not in self.nodes:
self.nodes.append(value)
# TODO: do this a different way. This remote file kludge isn't going
# to work long term
@classmethod
def remote(cls, contents: str, project_name: str) -> 'SourceFile':
self = cls(
path=RemoteFile(),
checksum=FileHash.from_contents(contents),
project_name=project_name,
contents=contents,
)
return self
@dataclass
class SchemaSourceFile(BaseSourceFile):
dfy: Dict[str, Any] = field(default_factory=dict)
# these are in the manifest.nodes dictionary
tests: Dict[str, Any] = field(default_factory=dict)
sources: List[str] = field(default_factory=list)
exposures: List[str] = field(default_factory=list)
# node patches contain models, seeds, snapshots, analyses
ndp: List[str] = field(default_factory=list)
# any macro patches in this file by macro unique_id.
mcp: Dict[str, str] = field(default_factory=dict)
# any source patches in this file. The entries are package, name pairs
# Patches are only against external sources. Sources can be
# created too, but those are in 'sources'
sop: List[SourceKey] = field(default_factory=list)
pp_dict: Optional[Dict[str, Any]] = None
pp_test_index: Optional[Dict[str, Any]] = None
@property
def dict_from_yaml(self):
return self.dfy
@property
def node_patches(self):
return self.ndp
@property
def macro_patches(self):
return self.mcp
@property
def source_patches(self):
return self.sop
def __post_serialize__(self, dct):
dct = super().__post_serialize__(dct)
# Remove partial parsing specific data
for key in ('pp_files', 'pp_test_index', 'pp_dict'):
if key in dct:
del dct[key]
return dct
def append_patch(self, yaml_key, unique_id):
self.node_patches.append(unique_id)
def add_test(self, node_unique_id, test_from):
name = test_from['name']
key = test_from['key']
if key not in self.tests:
self.tests[key] = {}
if name not in self.tests[key]:
self.tests[key][name] = []
self.tests[key][name].append(node_unique_id)
def remove_tests(self, yaml_key, name):
if yaml_key in self.tests:
if name in self.tests[yaml_key]:
del self.tests[yaml_key][name]
def get_tests(self, yaml_key, name):
if yaml_key in self.tests:
if name in self.tests[yaml_key]:
return self.tests[yaml_key][name]
return []
def get_key_and_name_for_test(self, test_unique_id):
yaml_key = None
block_name = None
for key in self.tests.keys():
for name in self.tests[key]:
for unique_id in self.tests[key][name]:
if unique_id == test_unique_id:
yaml_key = key
block_name = name
break
return (yaml_key, block_name)
def get_all_test_ids(self):
test_ids = []
for key in self.tests.keys():
for name in self.tests[key]:
test_ids.extend(self.tests[key][name])
return test_ids
AnySourceFile = Union[SchemaSourceFile, SourceFile]

View File

@@ -1,61 +1,49 @@
from dbt.contracts.graph.parsed import (
HasTestMetadata,
ParsedNode,
ParsedAnalysisNode,
ParsedModelNode,
ParsedDataTestNode,
ParsedHookNode,
ParsedModelNode,
ParsedExposure,
ParsedResource,
ParsedRPCNode,
ParsedSchemaTestNode,
ParsedSeedNode,
ParsedSnapshotNode,
ParsedSourceDefinition,
ParsedTestNode,
SeedConfig,
TestConfig,
TestMetadata,
PARSED_TYPES,
same_seeds,
)
from dbt.node_types import NodeType
from dbt.contracts.util import Replaceable
from dbt.exceptions import InternalException, RuntimeException
from hologram import JsonSchemaMixin
from dbt.dataclass_schema import dbtClassMixin
from dataclasses import dataclass, field
import sqlparse # type: ignore
from typing import Optional, List, Union, Dict, Type
@dataclass
class InjectedCTE(JsonSchemaMixin, Replaceable):
class InjectedCTE(dbtClassMixin, Replaceable):
id: str
sql: str
# for some frustrating reason, we can't subclass from ParsedNode directly,
# or typing.Union will flatten CompiledNode+ParsedNode into just ParsedNode.
# TODO: understand that issue and come up with some way for these two to share
# logic
@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):
compiled: bool = False
class CompiledNode(ParsedNode, CompiledNodeMixin):
compiled_sql: Optional[str] = None
extra_ctes_injected: bool = False
extra_ctes: List[InjectedCTE] = field(default_factory=list)
injected_sql: Optional[str] = None
wrapped_sql: Optional[str] = None
def prepend_ctes(self, prepended_ctes: List[InjectedCTE]):
self.extra_ctes_injected = True
self.extra_ctes = prepended_ctes
if self.compiled_sql is None:
raise RuntimeException(
'Cannot prepend ctes to an unparsed node', self
)
self.injected_sql = _inject_ctes_into_sql(
self.compiled_sql,
prepended_ctes,
)
self.validate(self.to_dict())
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
@@ -68,6 +56,12 @@ class CompiledNode(ParsedNode):
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):
@@ -94,21 +88,18 @@ class CompiledRPCNode(CompiledNode):
@dataclass
class CompiledSeedNode(CompiledNode):
# keep this in sync with ParsedSeedNode!
resource_type: NodeType = field(metadata={'restrict': [NodeType.Seed]})
config: SeedConfig = field(default_factory=SeedConfig)
seed_file_path: str = ''
def __post_init__(self):
if self.seed_file_path == '':
raise InternalException(
'Seeds should always have a seed_file_path'
)
@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):
@@ -116,124 +107,121 @@ class CompiledSnapshotNode(CompiledNode):
@dataclass
class CompiledTestNode(CompiledNode):
class CompiledDataTestNode(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 CompiledSchemaTestNode(CompiledNode, HasTestMetadata):
# keep this in sync with ParsedSchemaTestNode!
resource_type: NodeType = field(metadata={'restrict': [NodeType.Test]})
column_name: Optional[str] = None
config: TestConfig = field(default_factory=TestConfig)
test_metadata: Optional[TestMetadata] = 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
)
def _inject_ctes_into_sql(sql: str, ctes: List[InjectedCTE]) -> str:
"""
`ctes` is a list of InjectedCTEs like:
[
InjectedCTE(
id="cte_id_1",
sql="__dbt__CTE__ephemeral as (select * from table)",
),
InjectedCTE(
id="cte_id_2",
sql="__dbt__CTE__events as (select id, type from events)",
),
]
Given `sql` like:
"with internal_cte as (select * from sessions)
select * from internal_cte"
This will spit out:
"with __dbt__CTE__ephemeral as (select * from table),
__dbt__CTE__events as (select id, type from events),
with internal_cte as (select * from sessions)
select * from internal_cte"
(Whitespace enhanced for readability.)
"""
if len(ctes) == 0:
return sql
parsed_stmts = sqlparse.parse(sql)
parsed = parsed_stmts[0]
with_stmt = None
for token in parsed.tokens:
if token.is_keyword and token.normalized == 'WITH':
with_stmt = token
break
if with_stmt is None:
# no with stmt, add one, and inject CTEs right at the beginning
first_token = parsed.token_first()
with_stmt = sqlparse.sql.Token(sqlparse.tokens.Keyword, 'with')
parsed.insert_before(first_token, with_stmt)
else:
# stmt exists, add a comma (which will come after injected CTEs)
trailing_comma = sqlparse.sql.Token(sqlparse.tokens.Punctuation, ',')
parsed.insert_after(with_stmt, trailing_comma)
token = sqlparse.sql.Token(
sqlparse.tokens.Keyword,
", ".join(c.sql for c in ctes)
)
parsed.insert_after(with_stmt, token)
return str(parsed)
CompiledTestNode = Union[CompiledDataTestNode, CompiledSchemaTestNode]
COMPILED_TYPES: Dict[NodeType, Type[CompiledNode]] = {
NodeType.Analysis: CompiledAnalysisNode,
NodeType.Model: CompiledModelNode,
NodeType.Operation: CompiledHookNode,
NodeType.RPCCall: CompiledRPCNode,
NodeType.Seed: CompiledSeedNode,
NodeType.Snapshot: CompiledSnapshotNode,
NodeType.Test: CompiledTestNode,
PARSED_TYPES: Dict[Type[CompiledNode], Type[ParsedResource]] = {
CompiledAnalysisNode: ParsedAnalysisNode,
CompiledModelNode: ParsedModelNode,
CompiledHookNode: ParsedHookNode,
CompiledRPCNode: ParsedRPCNode,
CompiledSeedNode: ParsedSeedNode,
CompiledSnapshotNode: ParsedSnapshotNode,
CompiledDataTestNode: ParsedDataTestNode,
CompiledSchemaTestNode: ParsedSchemaTestNode,
}
def compiled_type_for(parsed: ParsedNode):
if parsed.resource_type in COMPILED_TYPES:
return COMPILED_TYPES[parsed.resource_type]
COMPILED_TYPES: Dict[Type[ParsedResource], Type[CompiledNode]] = {
ParsedAnalysisNode: CompiledAnalysisNode,
ParsedModelNode: CompiledModelNode,
ParsedHookNode: CompiledHookNode,
ParsedRPCNode: CompiledRPCNode,
ParsedSeedNode: CompiledSeedNode,
ParsedSnapshotNode: CompiledSnapshotNode,
ParsedDataTestNode: CompiledDataTestNode,
ParsedSchemaTestNode: CompiledSchemaTestNode,
}
# 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(compiled.resource_type)
cls = PARSED_TYPES.get(type(compiled))
if cls is None:
# how???
raise ValueError('invalid resource_type: {}'
.format(compiled.resource_type))
# validate=False to allow extra keys from compiling
return cls.from_dict(compiled.to_dict(), validate=False)
return cls.from_dict(compiled.to_dict(omit_none=True))
# This is anything that can be in manifest.nodes and isn't a Source.
NonSourceNode = Union[
NonSourceCompiledNode = Union[
CompiledAnalysisNode,
CompiledDataTestNode,
CompiledModelNode,
CompiledHookNode,
CompiledRPCNode,
CompiledSchemaTestNode,
CompiledSeedNode,
CompiledSnapshotNode,
CompiledTestNode,
]
NonSourceParsedNode = Union[
ParsedAnalysisNode,
ParsedModelNode,
ParsedDataTestNode,
ParsedHookNode,
ParsedModelNode,
ParsedRPCNode,
ParsedSchemaTestNode,
ParsedSeedNode,
ParsedSnapshotNode,
ParsedTestNode,
]
# 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[
NonSourceNode,
ManifestNode,
ParsedSourceDefinition,
]
# anything that participates in the graph: sources, exposures, manifest nodes
GraphMemberNode = Union[
CompileResultNode,
ParsedExposure,
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,566 @@
from dataclasses import field, Field, dataclass
from enum import Enum
from itertools import chain
from typing import (
Any, List, Optional, Dict, Union, Type, TypeVar, Callable
)
from dbt.dataclass_schema import (
dbtClassMixin, ValidationError, register_pattern,
)
from dbt.contracts.graph.unparsed import AdditionalPropertiesAllowed
from dbt.exceptions import InternalException, CompilationException
from dbt.contracts.util import Replaceable, list_str
from dbt import hooks
from dbt.node_types import NodeType
M = TypeVar('M', bound='Metadata')
def _get_meta_value(cls: Type[M], fld: Field, key: str, default: Any) -> M:
# a metadata field might exist. If it does, it might have a matching key.
# If it has both, make sure the value is valid and return it. If it
# doesn't, return the default.
if fld.metadata:
value = fld.metadata.get(key, default)
else:
value = default
try:
return cls(value)
except ValueError as 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]:
if existing is None:
result = {}
else:
result = existing.copy()
result.update({key: obj})
return result
class Metadata(Enum):
@classmethod
def from_field(cls: Type[M], fld: Field) -> M:
default = cls.default_field()
key = cls.metadata_key()
return _get_meta_value(cls, fld, key, default)
def meta(
self, existing: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
key = self.metadata_key()
return _set_meta_value(self, key, existing)
@classmethod
def default_field(cls) -> 'Metadata':
raise NotImplementedError('Not implemented')
@classmethod
def metadata_key(cls) -> str:
raise NotImplementedError('Not implemented')
class MergeBehavior(Metadata):
Append = 1
Update = 2
Clobber = 3
@classmethod
def default_field(cls) -> 'MergeBehavior':
return cls.Clobber
@classmethod
def metadata_key(cls) -> str:
return 'merge'
class ShowBehavior(Metadata):
Show = 1
Hide = 2
@classmethod
def default_field(cls) -> 'ShowBehavior':
return cls.Show
@classmethod
def metadata_key(cls) -> str:
return 'show_hide'
@classmethod
def should_show(cls, fld: Field) -> bool:
return cls.from_field(fld) == cls.Show
class CompareBehavior(Metadata):
Include = 1
Exclude = 2
@classmethod
def default_field(cls) -> 'CompareBehavior':
return cls.Include
@classmethod
def metadata_key(cls) -> str:
return 'compare'
@classmethod
def should_include(cls, fld: Field) -> bool:
return cls.from_field(fld) == cls.Include
def metas(*metas: Metadata) -> Dict[str, Any]:
existing: Dict[str, Any] = {}
for m in metas:
existing = m.meta(existing)
return existing
def _listify(value: Any) -> List:
if isinstance(value, list):
return value[:]
else:
return [value]
def _merge_field_value(
merge_behavior: MergeBehavior,
self_value: Any,
other_value: Any,
):
if merge_behavior == MergeBehavior.Clobber:
return other_value
elif merge_behavior == MergeBehavior.Append:
return _listify(self_value) + _listify(other_value)
elif merge_behavior == MergeBehavior.Update:
if not isinstance(self_value, dict):
raise InternalException(f'expected dict, got {self_value}')
if not isinstance(other_value, dict):
raise InternalException(f'expected dict, got {other_value}')
value = self_value.copy()
value.update(other_value)
return value
else:
raise InternalException(
f'Got an invalid merge_behavior: {merge_behavior}'
)
def insensitive_patterns(*patterns: str):
lowercased = []
for pattern in patterns:
lowercased.append(
''.join('[{}{}]'.format(s.upper(), s.lower()) for s in pattern)
)
return '^({})$'.format('|'.join(lowercased))
class Severity(str):
pass
register_pattern(Severity, insensitive_patterns('warn', 'error'))
@dataclass
class Hook(dbtClassMixin, Replaceable):
sql: str
transaction: bool = True
index: Optional[int] = None
T = TypeVar('T', bound='BaseConfig')
@dataclass
class BaseConfig(
AdditionalPropertiesAllowed, Replaceable
):
# enable syntax like: config['key']
def __getitem__(self, key):
return self.get(key)
# like doing 'get' on a dictionary
def get(self, key, default=None):
if hasattr(self, key):
return getattr(self, key)
elif key in self._extra:
return self._extra[key]
else:
return default
# enable syntax like: config['key'] = value
def __setitem__(self, key, value):
if hasattr(self, key):
setattr(self, key, value)
else:
self._extra[key] = value
def __delitem__(self, key):
if hasattr(self, key):
msg = (
'Error, tried to delete config key "{}": Cannot delete '
'built-in keys'
).format(key)
raise CompilationException(msg)
else:
del self._extra[key]
def _content_iterator(self, include_condition: Callable[[Field], bool]):
seen = set()
for fld, _ in self._get_fields():
seen.add(fld.name)
if include_condition(fld):
yield fld.name
for key in self._extra:
if key not in seen:
seen.add(key)
yield key
def __iter__(self):
yield from self._content_iterator(include_condition=lambda f: True)
def __len__(self):
return len(self._get_fields()) + len(self._extra)
@staticmethod
def compare_key(
unrendered: Dict[str, Any],
other: Dict[str, Any],
key: str,
) -> bool:
if key not in unrendered and key not in other:
return True
elif key not in unrendered and key in other:
return False
elif key in unrendered and key not in other:
return False
else:
return unrendered[key] == other[key]
@classmethod
def same_contents(
cls, unrendered: Dict[str, Any], other: Dict[str, Any]
) -> bool:
"""This is like __eq__, except it ignores some fields."""
seen = set()
for fld, target_name in cls._get_fields():
key = target_name
seen.add(key)
if CompareBehavior.should_include(fld):
if not cls.compare_key(unrendered, other, key):
return False
for key in chain(unrendered, other):
if key not in seen:
seen.add(key)
if not cls.compare_key(unrendered, other, key):
return False
return True
# This is used in 'add_config_call' to created the combined config_call_dict.
# 'meta' moved here from node
mergebehavior = {
"append": ['pre-hook', 'pre_hook', 'post-hook', 'post_hook', 'tags'],
"update": ['quoting', 'column_types', 'meta'],
}
@classmethod
def _merge_dicts(
cls, src: Dict[str, Any], data: Dict[str, Any]
) -> Dict[str, Any]:
"""Find all the items in data that match a target_field on this class,
and merge them with the data found in `src` for target_field, using the
field's specified merge behavior. Matching items will be removed from
`data` (but _not_ `src`!).
Returns a dict with the merge results.
That means this method mutates its input! Any remaining values in data
were not merged.
"""
result = {}
for fld, target_field in cls._get_fields():
if target_field not in data:
continue
data_attr = data.pop(target_field)
if target_field not in src:
result[target_field] = data_attr
continue
merge_behavior = MergeBehavior.from_field(fld)
self_attr = src[target_field]
result[target_field] = _merge_field_value(
merge_behavior=merge_behavior,
self_value=self_attr,
other_value=data_attr,
)
return result
def update_from(
self: T, data: Dict[str, Any], adapter_type: str, validate: bool = True
) -> T:
"""Given a dict of keys, update the current config from them, validate
it, and return a new config with the updated values
"""
# sadly, this is a circular import
from dbt.adapters.factory import get_config_class_by_name
dct = self.to_dict(omit_none=False)
adapter_config_cls = get_config_class_by_name(adapter_type)
self_merged = self._merge_dicts(dct, data)
dct.update(self_merged)
adapter_merged = adapter_config_cls._merge_dicts(dct, data)
dct.update(adapter_merged)
# any remaining fields must be "clobber"
dct.update(data)
# any validation failures must have come from the update
if validate:
self.validate(dct)
return self.from_dict(dct)
def finalize_and_validate(self: T) -> T:
dct = self.to_dict(omit_none=False)
self.validate(dct)
return self.from_dict(dct)
def replace(self, **kwargs):
dct = self.to_dict(omit_none=True)
mapping = self.field_mapping()
for key, value in kwargs.items():
new_key = mapping.get(key, key)
dct[new_key] = value
return self.from_dict(dct)
@dataclass
class SourceConfig(BaseConfig):
enabled: bool = True
@dataclass
class NodeAndTestConfig(BaseConfig):
enabled: bool = True
# these fields are included in serialized output, but are not part of
# config comparison (they are part of database_representation)
alias: Optional[str] = field(
default=None,
metadata=CompareBehavior.Exclude.meta(),
)
schema: Optional[str] = field(
default=None,
metadata=CompareBehavior.Exclude.meta(),
)
database: Optional[str] = field(
default=None,
metadata=CompareBehavior.Exclude.meta(),
)
tags: Union[List[str], str] = field(
default_factory=list_str,
metadata=metas(ShowBehavior.Hide,
MergeBehavior.Append,
CompareBehavior.Exclude),
)
meta: Dict[str, Any] = field(
default_factory=dict,
metadata=MergeBehavior.Update.meta(),
)
@dataclass
class NodeConfig(NodeAndTestConfig):
# Note: if any new fields are added with MergeBehavior, also update the
# 'mergebehavior' dictionary
materialized: str = 'view'
persist_docs: Dict[str, Any] = field(default_factory=dict)
post_hook: List[Hook] = field(
default_factory=list,
metadata=MergeBehavior.Append.meta(),
)
pre_hook: List[Hook] = field(
default_factory=list,
metadata=MergeBehavior.Append.meta(),
)
quoting: Dict[str, Any] = field(
default_factory=dict,
metadata=MergeBehavior.Update.meta(),
)
# This is actually only used by seeds. Should it be available to others?
# That would be a breaking change!
column_types: Dict[str, Any] = field(
default_factory=dict,
metadata=MergeBehavior.Update.meta(),
)
full_refresh: Optional[bool] = None
on_schema_change: Optional[str] = 'ignore'
@classmethod
def __pre_deserialize__(cls, data):
data = super().__pre_deserialize__(data)
field_map = {'post-hook': 'post_hook', 'pre-hook': 'pre_hook'}
# create a new dict because otherwise it gets overwritten in
# tests
new_dict = {}
for key in data:
new_dict[key] = data[key]
data = new_dict
for key in hooks.ModelHookType:
if key in data:
data[key] = [hooks.get_hook_dict(h) for h in data[key]]
for field_name in field_map:
if field_name in data:
new_name = field_map[field_name]
data[new_name] = data.pop(field_name)
return data
def __post_serialize__(self, dct):
dct = super().__post_serialize__(dct)
field_map = {'post_hook': 'post-hook', 'pre_hook': 'pre-hook'}
for field_name in field_map:
if field_name in dct:
dct[field_map[field_name]] = dct.pop(field_name)
return dct
# this is still used by jsonschema validation
@classmethod
def field_mapping(cls):
return {'post_hook': 'post-hook', 'pre_hook': 'pre-hook'}
@dataclass
class SeedConfig(NodeConfig):
materialized: str = 'seed'
quote_columns: Optional[bool] = None
@dataclass
class TestConfig(NodeAndTestConfig):
# this is repeated because of a different default
schema: Optional[str] = field(
default='dbt_test__audit',
metadata=CompareBehavior.Exclude.meta(),
)
materialized: str = 'test'
severity: Severity = Severity('ERROR')
store_failures: Optional[bool] = None
where: Optional[str] = None
limit: Optional[int] = None
fail_calc: str = 'count(*)'
warn_if: str = '!= 0'
error_if: str = '!= 0'
@classmethod
def same_contents(
cls, unrendered: Dict[str, Any], other: Dict[str, Any]
) -> bool:
"""This is like __eq__, except it explicitly checks certain fields."""
modifiers = [
'severity',
'where',
'limit',
'fail_calc',
'warn_if',
'error_if',
'store_failures'
]
seen = set()
for _, target_name in cls._get_fields():
key = target_name
seen.add(key)
if key in modifiers:
if not cls.compare_key(unrendered, other, key):
return False
return True
@dataclass
class EmptySnapshotConfig(NodeConfig):
materialized: str = 'snapshot'
@dataclass
class SnapshotConfig(EmptySnapshotConfig):
strategy: Optional[str] = None
unique_key: Optional[str] = None
target_schema: Optional[str] = None
target_database: Optional[str] = None
updated_at: Optional[str] = None
check_cols: Optional[Union[str, List[str]]] = None
@classmethod
def validate(cls, data):
super().validate(data)
if not data.get('strategy') or not data.get('unique_key') or not \
data.get('target_schema'):
raise ValidationError(
"Snapshots must be configured with a 'strategy', 'unique_key', "
"and 'target_schema'.")
if data.get('strategy') == 'check':
if not data.get('check_cols'):
raise ValidationError(
"A snapshot configured with the check strategy must "
"specify a check_cols configuration.")
if (isinstance(data['check_cols'], str) and
data['check_cols'] != 'all'):
raise ValidationError(
f"Invalid value for 'check_cols': {data['check_cols']}. "
"Expected 'all' or a list of strings.")
elif data.get('strategy') == 'timestamp':
if not data.get('updated_at'):
raise ValidationError(
"A snapshot configured with the timestamp strategy "
"must specify an updated_at configuration.")
if data.get('check_cols'):
raise ValidationError(
"A 'timestamp' snapshot should not have 'check_cols'")
# If the strategy is not 'check' or 'timestamp' it's a custom strategy,
# formerly supported with GenericSnapshotConfig
def finalize_and_validate(self):
data = self.to_dict(omit_none=True)
self.validate(data)
return self.from_dict(data)
RESOURCE_TYPES: Dict[NodeType, Type[BaseConfig]] = {
NodeType.Source: SourceConfig,
NodeType.Seed: SeedConfig,
NodeType.Test: TestConfig,
NodeType.Model: NodeConfig,
NodeType.Snapshot: SnapshotConfig,
}
# base resource types are like resource types, except nothing has mandatory
# configs.
BASE_RESOURCE_TYPES: Dict[NodeType, Type[BaseConfig]] = RESOURCE_TYPES.copy()
BASE_RESOURCE_TYPES.update({
NodeType.Snapshot: EmptySnapshotConfig
})
def get_config_for(resource_type: NodeType, base=False) -> Type[BaseConfig]:
if base:
lookup = BASE_RESOURCE_TYPES
else:
lookup = RESOURCE_TYPES
return lookup.get(resource_type, NodeConfig)

View File

@@ -1,141 +1,80 @@
import os
from dataclasses import dataclass, field, Field
import time
from dataclasses import dataclass, field
from mashumaro.types import SerializableType
from pathlib import Path
from typing import (
Optional,
Union,
List,
Dict,
Any,
Type,
Sequence,
Tuple,
NewType,
MutableMapping,
Iterator,
TypeVar,
)
from hologram import JsonSchemaMixin
from hologram.helpers import (
StrEnum, register_pattern
from dbt.dataclass_schema import (
dbtClassMixin, ExtensibleDbtClassMixin
)
from dbt.clients.system import write_file
import dbt.flags
from dbt.contracts.files import FileHash, MAXIMUM_SEED_SIZE_NAME
from dbt.contracts.graph.unparsed import (
UnparsedNode, UnparsedMacro, UnparsedDocumentationFile, Quoting, Docs,
UnparsedNode, UnparsedDocumentation, Quoting, Docs,
UnparsedBaseNode, FreshnessThreshold, ExternalTable,
AdditionalPropertiesAllowed, HasYamlMetadata, MacroArgument
HasYamlMetadata, MacroArgument, UnparsedSourceDefinition,
UnparsedSourceTableDefinition, UnparsedColumn, TestDef,
ExposureOwner, ExposureType, MaturityType
)
from dbt.contracts.util import Replaceable, list_str
from dbt.contracts.util import Replaceable, AdditionalPropertiesMixin
from dbt.exceptions import warn_or_error
from dbt.logger import GLOBAL_LOGGER as logger # noqa
from dbt import flags
from dbt.node_types import NodeType
class SnapshotStrategy(StrEnum):
Timestamp = 'timestamp'
Check = 'check'
class All(StrEnum):
All = 'all'
from .model_config import (
NodeConfig,
SeedConfig,
TestConfig,
SourceConfig,
EmptySnapshotConfig,
SnapshotConfig,
)
@dataclass
class Hook(JsonSchemaMixin, Replaceable):
sql: str
transaction: bool = True
index: Optional[int] = None
def insensitive_patterns(*patterns: str):
lowercased = []
for pattern in patterns:
lowercased.append(
''.join('[{}{}]'.format(s.upper(), s.lower()) for s in pattern)
)
return '^({})$'.format('|'.join(lowercased))
Severity = NewType('Severity', str)
register_pattern(Severity, insensitive_patterns('warn', 'error'))
@dataclass
class NodeConfig(
AdditionalPropertiesAllowed, Replaceable, MutableMapping[str, Any]
class ColumnInfo(
AdditionalPropertiesMixin,
ExtensibleDbtClassMixin,
Replaceable
):
enabled: bool = True
materialized: str = 'view'
persist_docs: Dict[str, Any] = field(default_factory=dict)
post_hook: List[Hook] = field(default_factory=list)
pre_hook: List[Hook] = field(default_factory=list)
vars: Dict[str, Any] = field(default_factory=dict)
quoting: Dict[str, Any] = field(default_factory=dict)
column_types: Dict[str, Any] = field(default_factory=dict)
tags: Union[List[str], str] = field(default_factory=list_str)
@classmethod
def field_mapping(cls):
return {'post_hook': 'post-hook', 'pre_hook': 'pre-hook'}
# Implement MutableMapping so this config will behave as some macros expect
# during parsing (notably, syntax like `{{ node.config['schema'] }}`)
def __getitem__(self, key):
"""Handle parse-time use of `config` as a dictionary, making the extra
values available during parsing.
"""
if hasattr(self, key):
return getattr(self, key)
else:
return self._extra[key]
def __setitem__(self, key, value):
if hasattr(self, key):
setattr(self, key, value)
else:
self._extra[key] = value
def __delitem__(self, key):
if hasattr(self, key):
msg = (
'Error, tried to delete config key "{}": Cannot delete '
'built-in keys'
).format(key)
raise dbt.exceptions.CompilationException(msg)
else:
del self._extra[key]
def __iter__(self):
for fld, _ in self._get_fields():
yield fld.name
for key in self._extra:
yield key
def __len__(self):
return len(self._get_fields()) + len(self._extra)
@dataclass
class ColumnInfo(JsonSchemaMixin, Replaceable):
name: str
description: str = ''
meta: Dict[str, Any] = field(default_factory=dict)
data_type: Optional[str] = None
quote: Optional[bool] = None
tags: List[str] = field(default_factory=list)
_extra: Dict[str, Any] = field(default_factory=dict)
@dataclass
class HasFqn(JsonSchemaMixin, Replaceable):
class HasFqn(dbtClassMixin, Replaceable):
fqn: List[str]
def same_fqn(self, other: 'HasFqn') -> bool:
return self.fqn == other.fqn
@dataclass
class HasUniqueID(JsonSchemaMixin, Replaceable):
class HasUniqueID(dbtClassMixin, Replaceable):
unique_id: str
@dataclass
class MacroDependsOn(JsonSchemaMixin, Replaceable):
class MacroDependsOn(dbtClassMixin, Replaceable):
macros: List[str] = field(default_factory=list)
# 'in' on lists is O(n) so this is O(n^2) for # of macros
@@ -154,12 +93,22 @@ class DependsOn(MacroDependsOn):
@dataclass
class HasRelationMetadata(JsonSchemaMixin, Replaceable):
database: str
class HasRelationMetadata(dbtClassMixin, Replaceable):
database: Optional[str]
schema: str
# Can't set database to None like it ought to be
# because it messes up the subclasses and default parameters
# so hack it here
@classmethod
def __pre_deserialize__(cls, data):
data = super().__pre_deserialize__(data)
if 'database' not in data:
data['database'] = None
return data
class ParsedNodeMixins(JsonSchemaMixin):
class ParsedNodeMixins(dbtClassMixin):
resource_type: NodeType
depends_on: DependsOn
config: NodeConfig
@@ -168,6 +117,21 @@ class ParsedNodeMixins(JsonSchemaMixin):
def is_refable(self):
return self.resource_type in NodeType.refable()
@property
def should_store_failures(self):
return self.resource_type == NodeType.Test and (
self.config.store_failures if self.config.store_failures is not None
else flags.STORE_FAILURES
)
# will this node map to an object in the database?
@property
def is_relational(self):
return (
self.resource_type in NodeType.refable() or
self.should_store_failures
)
@property
def is_ephemeral(self):
return self.config.materialized == 'ephemeral'
@@ -184,21 +148,25 @@ class ParsedNodeMixins(JsonSchemaMixin):
"""Given a ParsedNodePatch, add the new information to the node."""
# explicitly pick out the parts to update so we don't inadvertently
# step on the model name or anything
self.patch_path: Optional[str] = patch.original_file_path
# Note: config should already be updated
self.patch_path: Optional[str] = patch.file_id
# update created_at so process_docs will run in partial parsing
self.created_at = int(time.time())
self.description = patch.description
self.columns = patch.columns
self.meta = patch.meta
self.docs = patch.docs
if dbt.flags.STRICT_MODE:
assert isinstance(self, JsonSchemaMixin)
self.to_dict(validate=True)
if flags.STRICT_MODE:
# It seems odd that an instance can be invalid
# Maybe there should be validation or restrictions
# elsewhere?
assert isinstance(self, dbtClassMixin)
dct = self.to_dict(omit_none=False)
self.validate(dct)
def get_materialization(self):
return self.config.materialized
def local_vars(self):
return self.config.vars
@dataclass
class ParsedNodeMandatory(
@@ -209,6 +177,8 @@ class ParsedNodeMandatory(
Replaceable
):
alias: str
checksum: FileHash
config: NodeConfig = field(default_factory=NodeConfig)
@property
def identifier(self):
@@ -217,7 +187,6 @@ class ParsedNodeMandatory(
@dataclass
class ParsedNodeDefaults(ParsedNodeMandatory):
config: NodeConfig = field(default_factory=NodeConfig)
tags: List[str] = field(default_factory=list)
refs: List[List[str]] = field(default_factory=list)
sources: List[List[Any]] = field(default_factory=list)
@@ -227,20 +196,138 @@ class ParsedNodeDefaults(ParsedNodeMandatory):
meta: Dict[str, Any] = field(default_factory=dict)
docs: Docs = field(default_factory=Docs)
patch_path: Optional[str] = None
compiled_path: Optional[str] = None
build_path: Optional[str] = None
deferred: bool = False
unrendered_config: Dict[str, Any] = field(default_factory=dict)
created_at: int = field(default_factory=lambda: int(time.time()))
config_call_dict: Dict[str, Any] = field(default_factory=dict)
def write_node(self, target_path: str, subdirectory: str, payload: str):
if (os.path.basename(self.path) ==
os.path.basename(self.original_file_path)):
# One-to-one relationship of nodes to files.
path = self.original_file_path
else:
# Many-to-one relationship of nodes to files.
path = os.path.join(self.original_file_path, self.path)
full_path = os.path.join(
target_path, subdirectory, self.package_name, self.path
target_path, subdirectory, self.package_name, path
)
write_file(full_path, payload)
return full_path
T = TypeVar('T', bound='ParsedNode')
@dataclass
class ParsedNode(ParsedNodeDefaults, ParsedNodeMixins):
pass
class ParsedNode(ParsedNodeDefaults, ParsedNodeMixins, SerializableType):
def _serialize(self):
return self.to_dict()
def __post_serialize__(self, dct):
if 'config_call_dict' in dct:
del dct['config_call_dict']
return dct
@classmethod
def _deserialize(cls, dct: Dict[str, int]):
# The serialized ParsedNodes do not differ from each other
# in fields that would allow 'from_dict' to distinguis
# between them.
resource_type = dct['resource_type']
if resource_type == 'model':
return ParsedModelNode.from_dict(dct)
elif resource_type == 'analysis':
return ParsedAnalysisNode.from_dict(dct)
elif resource_type == 'seed':
return ParsedSeedNode.from_dict(dct)
elif resource_type == 'rpc':
return ParsedRPCNode.from_dict(dct)
elif resource_type == 'test':
if 'test_metadata' in dct:
return ParsedSchemaTestNode.from_dict(dct)
else:
return ParsedDataTestNode.from_dict(dct)
elif resource_type == 'operation':
return ParsedHookNode.from_dict(dct)
elif resource_type == 'seed':
return ParsedSeedNode.from_dict(dct)
elif resource_type == 'snapshot':
return ParsedSnapshotNode.from_dict(dct)
else:
return cls.from_dict(dct)
def _persist_column_docs(self) -> bool:
if hasattr(self.config, 'persist_docs'):
assert isinstance(self.config, NodeConfig)
return bool(self.config.persist_docs.get('columns'))
return False
def _persist_relation_docs(self) -> bool:
if hasattr(self.config, 'persist_docs'):
assert isinstance(self.config, NodeConfig)
return bool(self.config.persist_docs.get('relation'))
return False
def same_body(self: T, other: T) -> bool:
return self.raw_sql == other.raw_sql
def same_persisted_description(self: T, other: T) -> bool:
# the check on configs will handle the case where we have different
# persist settings, so we only have to care about the cases where they
# are the same..
if self._persist_relation_docs():
if self.description != other.description:
return False
if self._persist_column_docs():
# assert other._persist_column_docs()
column_descriptions = {
k: v.description for k, v in self.columns.items()
}
other_column_descriptions = {
k: v.description for k, v in other.columns.items()
}
if column_descriptions != other_column_descriptions:
return False
return True
def same_database_representation(self, other: T) -> bool:
# compare the config representation, not the node's config value. This
# compares the configured value, rather than the ultimate value (so
# generate_*_name and unset values derived from the target are
# ignored)
keys = ('database', 'schema', 'alias')
for key in keys:
mine = self.unrendered_config.get(key)
others = other.unrendered_config.get(key)
if mine != others:
return False
return True
def same_config(self, old: T) -> bool:
return self.config.same_contents(
self.unrendered_config,
old.unrendered_config,
)
def same_contents(self: T, old: Optional[T]) -> bool:
if old is None:
return False
return (
self.same_body(old) and
self.same_config(old) and
self.same_persisted_description(old) and
self.same_fqn(old) and
self.same_database_representation(old) and
True
)
@dataclass
@@ -266,138 +353,97 @@ class ParsedRPCNode(ParsedNode):
resource_type: NodeType = field(metadata={'restrict': [NodeType.RPCCall]})
class SeedConfig(NodeConfig):
quote_columns: Optional[bool] = None
def same_seeds(first: ParsedNode, second: ParsedNode) -> bool:
# for seeds, we check the hashes. If the hashes are different types,
# no match. If the hashes are both the same 'path', log a warning and
# assume they are the same
# if the current checksum is a path, we want to log a warning.
result = first.checksum == second.checksum
if first.checksum.name == 'path':
msg: str
if second.checksum.name != 'path':
msg = (
f'Found a seed ({first.package_name}.{first.name}) '
f'>{MAXIMUM_SEED_SIZE_NAME} in size. The previous file was '
f'<={MAXIMUM_SEED_SIZE_NAME}, so it has changed'
)
elif result:
msg = (
f'Found a seed ({first.package_name}.{first.name}) '
f'>{MAXIMUM_SEED_SIZE_NAME} in size at the same path, dbt '
f'cannot tell if it has changed: assuming they are the same'
)
elif not result:
msg = (
f'Found a seed ({first.package_name}.{first.name}) '
f'>{MAXIMUM_SEED_SIZE_NAME} in size. The previous file was in '
f'a different location, assuming it has changed'
)
else:
msg = (
f'Found a seed ({first.package_name}.{first.name}) '
f'>{MAXIMUM_SEED_SIZE_NAME} in size. The previous file had a '
f'checksum type of {second.checksum.name}, so it has changed'
)
warn_or_error(msg, node=first)
return result
@dataclass
class ParsedSeedNode(ParsedNode):
# keep this in sync with CompiledSeedNode!
resource_type: NodeType = field(metadata={'restrict': [NodeType.Seed]})
config: SeedConfig = field(default_factory=SeedConfig)
seed_file_path: str = ''
def __post_init__(self):
if self.seed_file_path == '':
raise dbt.exceptions.InternalException(
'Seeds should always have a seed_file_path'
)
@property
def empty(self):
""" Seeds are never empty"""
return False
@dataclass
class TestConfig(NodeConfig):
severity: Severity = Severity('error')
def same_body(self: T, other: T) -> bool:
return same_seeds(self, other)
@dataclass
class TestMetadata(JsonSchemaMixin):
namespace: Optional[str]
class TestMetadata(dbtClassMixin, Replaceable):
name: str
kwargs: Dict[str, Any]
kwargs: Dict[str, Any] = field(default_factory=dict)
namespace: Optional[str] = None
@dataclass
class ParsedTestNode(ParsedNode):
class HasTestMetadata(dbtClassMixin):
test_metadata: TestMetadata
@dataclass
class ParsedDataTestNode(ParsedNode):
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 ParsedSchemaTestNode(ParsedNode, HasTestMetadata):
# keep this in sync with CompiledSchemaTestNode!
resource_type: NodeType = field(metadata={'restrict': [NodeType.Test]})
column_name: Optional[str] = None
config: TestConfig = field(default_factory=TestConfig)
test_metadata: Optional[TestMetadata] = 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
@dataclass(init=False)
class _SnapshotConfig(NodeConfig):
unique_key: str = field(init=False, metadata=dict(init_required=True))
target_schema: str = field(init=False, metadata=dict(init_required=True))
target_database: Optional[str] = None
def __init__(
self,
unique_key: str,
target_schema: str,
target_database: Optional[str] = None,
**kwargs
) -> None:
self.unique_key = unique_key
self.target_schema = target_schema
self.target_database = target_database
super().__init__(**kwargs)
# type hacks...
@classmethod
def _get_fields(cls) -> List[Tuple[Field, str]]: # type: ignore
fields: List[Tuple[Field, str]] = []
for old_field, name in super()._get_fields():
new_field = old_field
# tell hologram we're really an initvar
if old_field.metadata and old_field.metadata.get('init_required'):
new_field = field(init=True, metadata=old_field.metadata)
new_field.name = old_field.name
new_field.type = old_field.type
new_field._field_type = old_field._field_type # type: ignore
fields.append((new_field, name))
return fields
@dataclass(init=False)
class GenericSnapshotConfig(_SnapshotConfig):
strategy: str = field(init=False, metadata=dict(init_required=True))
def __init__(self, strategy: str, **kwargs) -> None:
self.strategy = strategy
super().__init__(**kwargs)
@dataclass(init=False)
class TimestampSnapshotConfig(_SnapshotConfig):
strategy: str = field(
init=False,
metadata=dict(
restrict=[str(SnapshotStrategy.Timestamp)],
init_required=True,
),
)
updated_at: str = field(init=False, metadata=dict(init_required=True))
def __init__(
self, strategy: str, updated_at: str, **kwargs
) -> None:
self.strategy = strategy
self.updated_at = updated_at
super().__init__(**kwargs)
@dataclass(init=False)
class CheckSnapshotConfig(_SnapshotConfig):
strategy: str = field(
init=False,
metadata=dict(
restrict=[str(SnapshotStrategy.Check)],
init_required=True,
),
)
# TODO: is there a way to get this to accept tuples of strings? Adding
# `Tuple[str, ...]` to the list of types results in this:
# ['email'] is valid under each of {'type': 'array', 'items':
# {'type': 'string'}}, {'type': 'array', 'items': {'type': 'string'}}
# but without it, parsing gets upset about values like `('email',)`
# maybe hologram itself should support this behavior? It's not like tuples
# are meaningful in json
check_cols: Union[All, List[str]] = field(
init=False,
metadata=dict(init_required=True),
)
def __init__(
self, strategy: str, check_cols: Union[All, List[str]],
**kwargs
) -> None:
self.strategy = strategy
self.check_cols = check_cols
super().__init__(**kwargs)
return (
self.same_config(other) and
self.same_fqn(other) and
True
)
@dataclass
@@ -409,59 +455,13 @@ class IntermediateSnapshotNode(ParsedNode):
# uses a regular node config, which the snapshot parser will then convert
# into a full ParsedSnapshotNode after rendering.
resource_type: NodeType = field(metadata={'restrict': [NodeType.Snapshot]})
def _create_if_else_chain(
key: str,
criteria: List[Tuple[str, Type[JsonSchemaMixin]]],
default: Type[JsonSchemaMixin]
) -> Dict[str, Any]:
"""Mutate a given schema key that contains a 'oneOf' to instead be an
'if-then-else' chain. This results is much better/more consistent errors
from jsonschema.
"""
schema: Dict[str, Any] = {}
result: Dict[str, Any] = {}
criteria = criteria[:]
while criteria:
if_clause, then_clause = criteria.pop()
schema['if'] = {'properties': {
key: {'enum': [if_clause]}
}}
schema['then'] = then_clause.json_schema()
schema['else'] = {}
schema = schema['else']
schema.update(default.json_schema())
return result
config: EmptySnapshotConfig = field(default_factory=EmptySnapshotConfig)
@dataclass
class ParsedSnapshotNode(ParsedNode):
resource_type: NodeType = field(metadata={'restrict': [NodeType.Snapshot]})
config: Union[
CheckSnapshotConfig,
TimestampSnapshotConfig,
GenericSnapshotConfig,
]
@classmethod
def json_schema(cls, embeddable: bool = False) -> Dict[str, Any]:
schema = super().json_schema(embeddable)
# mess with config
configs: List[Tuple[str, Type[JsonSchemaMixin]]] = [
(str(SnapshotStrategy.Check), CheckSnapshotConfig),
(str(SnapshotStrategy.Timestamp), TimestampSnapshotConfig),
]
if embeddable:
dest = schema[cls.__name__]['properties']
else:
dest = schema['properties']
dest['config'] = _create_if_else_chain(
'strategy', configs, GenericSnapshotConfig
)
return schema
config: SnapshotConfig
@dataclass
@@ -470,6 +470,7 @@ class ParsedPatch(HasYamlMetadata, Replaceable):
description: str
meta: Dict[str, Any]
docs: Docs
config: Dict[str, Any]
# The parsed node update is only the 'patch', not the test. The test became a
@@ -486,7 +487,7 @@ class ParsedMacroPatch(ParsedPatch):
@dataclass
class ParsedMacro(UnparsedMacro, HasUniqueID):
class ParsedMacro(UnparsedBaseNode, HasUniqueID):
name: str
macro_sql: str
resource_type: NodeType = field(metadata={'restrict': [NodeType.Macro]})
@@ -496,24 +497,34 @@ class ParsedMacro(UnparsedMacro, HasUniqueID):
depends_on: MacroDependsOn = field(default_factory=MacroDependsOn)
description: str = ''
meta: Dict[str, Any] = field(default_factory=dict)
docs: Docs = field(default_factory=Docs)
patch_path: Optional[str] = None
arguments: List[MacroArgument] = field(default_factory=list)
def local_vars(self):
return {}
created_at: int = field(default_factory=lambda: int(time.time()))
def patch(self, patch: ParsedMacroPatch):
self.patch_path: Optional[str] = patch.original_file_path
self.patch_path: Optional[str] = patch.file_id
self.description = patch.description
self.created_at = int(time.time())
self.meta = patch.meta
self.docs = patch.docs
self.arguments = patch.arguments
if dbt.flags.STRICT_MODE:
assert isinstance(self, JsonSchemaMixin)
self.to_dict(validate=True)
if flags.STRICT_MODE:
# What does this actually validate?
assert isinstance(self, dbtClassMixin)
dct = self.to_dict(omit_none=False)
self.validate(dct)
def same_contents(self, other: Optional['ParsedMacro']) -> bool:
if other is None:
return False
# the only thing that makes one macro different from another with the
# same name/package is its content
return self.macro_sql == other.macro_sql
@dataclass
class ParsedDocumentation(UnparsedDocumentationFile, HasUniqueID):
class ParsedDocumentation(UnparsedDocumentation, HasUniqueID):
name: str
block_contents: str
@@ -521,13 +532,81 @@ class ParsedDocumentation(UnparsedDocumentationFile, HasUniqueID):
def search_name(self):
return self.name
def same_contents(self, other: Optional['ParsedDocumentation']) -> bool:
if other is None:
return False
# the only thing that makes one doc different from another with the
# same name/package is its content
return self.block_contents == other.block_contents
def normalize_test(testdef: TestDef) -> Dict[str, Any]:
if isinstance(testdef, str):
return {testdef: {}}
else:
return testdef
@dataclass
class UnpatchedSourceDefinition(UnparsedBaseNode, HasUniqueID, HasFqn):
source: UnparsedSourceDefinition
table: UnparsedSourceTableDefinition
resource_type: NodeType = field(metadata={'restrict': [NodeType.Source]})
patch_path: Optional[Path] = None
def get_full_source_name(self):
return f'{self.source.name}_{self.table.name}'
def get_source_representation(self):
return f'source("{self.source.name}", "{self.table.name}")'
@property
def name(self) -> str:
return self.get_full_source_name()
@property
def quote_columns(self) -> Optional[bool]:
result = None
if self.source.quoting.column is not None:
result = self.source.quoting.column
if self.table.quoting.column is not None:
result = self.table.quoting.column
return result
@property
def columns(self) -> Sequence[UnparsedColumn]:
if self.table.columns is None:
return []
else:
return self.table.columns
def get_tests(
self
) -> Iterator[Tuple[Dict[str, Any], Optional[UnparsedColumn]]]:
for test in self.tests:
yield normalize_test(test), None
for column in self.columns:
if column.tests is not None:
for test in column.tests:
yield normalize_test(test), column
@property
def tests(self) -> List[TestDef]:
if self.table.tests is None:
return []
else:
return self.table.tests
@dataclass
class ParsedSourceDefinition(
UnparsedBaseNode,
HasUniqueID,
HasRelationMetadata,
HasFqn):
UnparsedBaseNode,
HasUniqueID,
HasRelationMetadata,
HasFqn,
):
name: str
source_name: str
source_description: str
@@ -543,6 +622,69 @@ class ParsedSourceDefinition(
meta: Dict[str, Any] = field(default_factory=dict)
source_meta: Dict[str, Any] = field(default_factory=dict)
tags: List[str] = field(default_factory=list)
config: SourceConfig = field(default_factory=SourceConfig)
patch_path: Optional[Path] = None
unrendered_config: Dict[str, Any] = field(default_factory=dict)
relation_name: Optional[str] = None
created_at: int = field(default_factory=lambda: int(time.time()))
def same_database_representation(
self, other: 'ParsedSourceDefinition'
) -> bool:
return (
self.database == other.database and
self.schema == other.schema and
self.identifier == other.identifier and
True
)
def same_quoting(self, other: 'ParsedSourceDefinition') -> bool:
return self.quoting == other.quoting
def same_freshness(self, other: 'ParsedSourceDefinition') -> bool:
return (
self.freshness == other.freshness and
self.loaded_at_field == other.loaded_at_field and
True
)
def same_external(self, other: 'ParsedSourceDefinition') -> bool:
return self.external == other.external
def same_config(self, old: 'ParsedSourceDefinition') -> bool:
return self.config.same_contents(
self.unrendered_config,
old.unrendered_config,
)
def same_contents(self, old: Optional['ParsedSourceDefinition']) -> bool:
# existing when it didn't before is a change!
if old is None:
return True
# config changes are changes (because the only config is "enabled", and
# enabling a source is a change!)
# changing the database/schema/identifier is a change
# messing around with external stuff is a change (uh, right?)
# quoting changes are changes
# freshness changes are changes, I guess
# metadata/tags changes are not "changes"
# patching/description changes are not "changes"
return (
self.same_database_representation(old) and
self.same_fqn(old) and
self.same_config(old) and
self.same_quoting(old) and
self.same_freshness(old) and
self.same_external(old) and
True
)
def get_full_source_name(self):
return f'{self.source_name}_{self.name}'
def get_source_representation(self):
return f'source("{self.source.name}", "{self.table.name}")'
@property
def is_refable(self):
@@ -560,6 +702,10 @@ class ParsedSourceDefinition(
def depends_on_nodes(self):
return []
@property
def depends_on(self):
return DependsOn(macros=[], nodes=[])
@property
def refs(self):
return []
@@ -577,20 +723,82 @@ class ParsedSourceDefinition(
return f'{self.source_name}.{self.name}'
ParsedResource = Union[
ParsedMacro, ParsedNode, ParsedDocumentation, ParsedSourceDefinition
@dataclass
class ParsedExposure(UnparsedBaseNode, HasUniqueID, HasFqn):
name: str
type: ExposureType
owner: ExposureOwner
resource_type: NodeType = NodeType.Exposure
description: str = ''
maturity: Optional[MaturityType] = None
meta: Dict[str, Any] = field(default_factory=dict)
tags: List[str] = field(default_factory=list)
url: Optional[str] = None
depends_on: DependsOn = field(default_factory=DependsOn)
refs: List[List[str]] = field(default_factory=list)
sources: List[List[str]] = field(default_factory=list)
created_at: int = field(default_factory=lambda: int(time.time()))
@property
def depends_on_nodes(self):
return self.depends_on.nodes
@property
def search_name(self):
return self.name
def same_depends_on(self, old: 'ParsedExposure') -> bool:
return set(self.depends_on.nodes) == set(old.depends_on.nodes)
def same_description(self, old: 'ParsedExposure') -> bool:
return self.description == old.description
def same_maturity(self, old: 'ParsedExposure') -> bool:
return self.maturity == old.maturity
def same_owner(self, old: 'ParsedExposure') -> bool:
return self.owner == old.owner
def same_exposure_type(self, old: 'ParsedExposure') -> bool:
return self.type == old.type
def same_url(self, old: 'ParsedExposure') -> bool:
return self.url == old.url
def same_contents(self, old: Optional['ParsedExposure']) -> bool:
# existing when it didn't before is a change!
# metadata/tags changes are not "changes"
if old is None:
return True
return (
self.same_fqn(old) and
self.same_exposure_type(old) and
self.same_owner(old) and
self.same_maturity(old) and
self.same_url(old) and
self.same_description(old) and
self.same_depends_on(old) and
True
)
ManifestNodes = Union[
ParsedAnalysisNode,
ParsedDataTestNode,
ParsedHookNode,
ParsedModelNode,
ParsedRPCNode,
ParsedSchemaTestNode,
ParsedSeedNode,
ParsedSnapshotNode,
]
PARSED_TYPES: Dict[NodeType, Type[ParsedResource]] = {
NodeType.Analysis: ParsedAnalysisNode,
NodeType.Documentation: ParsedDocumentation,
NodeType.Macro: ParsedMacro,
NodeType.Model: ParsedModelNode,
NodeType.Operation: ParsedHookNode,
NodeType.RPCCall: ParsedRPCNode,
NodeType.Seed: ParsedSeedNode,
NodeType.Snapshot: ParsedSnapshotNode,
NodeType.Source: ParsedSourceDefinition,
NodeType.Test: ParsedTestNode,
}
ParsedResource = Union[
ParsedDocumentation,
ParsedMacro,
ParsedNode,
ParsedExposure,
ParsedSourceDefinition,
]

View File

@@ -1,22 +1,34 @@
from dbt.node_types import NodeType
from dbt.contracts.util import Replaceable, Mergeable
from dbt.contracts.util import (
AdditionalPropertiesMixin,
Mergeable,
Replaceable,
)
# trigger the PathEncoder
import dbt.helper_types # noqa:F401
from dbt.exceptions import CompilationException
from hologram import JsonSchemaMixin
from hologram.helpers import StrEnum, ExtensibleJsonSchemaMixin
from dbt.dataclass_schema import (
dbtClassMixin, StrEnum, ExtensibleDbtClassMixin
)
from dataclasses import dataclass, field
from datetime import timedelta
from pathlib import Path
from typing import Optional, List, Union, Dict, Any, Sequence
@dataclass
class UnparsedBaseNode(JsonSchemaMixin, Replaceable):
class UnparsedBaseNode(dbtClassMixin, Replaceable):
package_name: str
root_path: str
path: str
original_file_path: str
@property
def file_id(self):
return f'{self.package_name}://{self.original_file_path}'
@dataclass
class HasSQL:
@@ -59,17 +71,19 @@ class UnparsedRunHook(UnparsedNode):
@dataclass
class Docs(JsonSchemaMixin, Replaceable):
class Docs(dbtClassMixin, Replaceable):
show: bool = True
@dataclass
class HasDocs(JsonSchemaMixin, Replaceable):
class HasDocs(AdditionalPropertiesMixin, ExtensibleDbtClassMixin,
Replaceable):
name: str
description: str = ''
meta: Dict[str, Any] = field(default_factory=dict)
data_type: Optional[str] = None
docs: Docs = field(default_factory=Docs)
_extra: Dict[str, Any] = field(default_factory=dict)
TestDef = Union[Dict[str, Any], str]
@@ -91,7 +105,7 @@ class UnparsedColumn(HasTests):
@dataclass
class HasColumnDocs(JsonSchemaMixin, Replaceable):
class HasColumnDocs(dbtClassMixin, Replaceable):
columns: Sequence[HasDocs] = field(default_factory=list)
@@ -101,31 +115,40 @@ class HasColumnTests(HasColumnDocs):
@dataclass
class HasYamlMetadata(JsonSchemaMixin):
class HasYamlMetadata(dbtClassMixin):
original_file_path: str
yaml_key: str
package_name: str
@property
def file_id(self):
return f'{self.package_name}://{self.original_file_path}'
@dataclass
class UnparsedAnalysisUpdate(HasColumnDocs, HasDocs, HasYamlMetadata):
class HasConfig():
config: Dict[str, Any] = field(default_factory=dict)
@dataclass
class UnparsedAnalysisUpdate(HasConfig, HasColumnDocs, HasDocs, HasYamlMetadata):
pass
@dataclass
class UnparsedNodeUpdate(HasColumnTests, HasTests, HasYamlMetadata):
class UnparsedNodeUpdate(HasConfig, HasColumnTests, HasTests, HasYamlMetadata):
quote_columns: Optional[bool] = None
@dataclass
class MacroArgument(JsonSchemaMixin):
class MacroArgument(dbtClassMixin):
name: str
type: Optional[str] = None
description: str = ''
@dataclass
class UnparsedMacroUpdate(HasDocs, HasYamlMetadata):
class UnparsedMacroUpdate(HasConfig, HasDocs, HasYamlMetadata):
arguments: List[MacroArgument] = field(default_factory=list)
@@ -139,7 +162,7 @@ class TimePeriod(StrEnum):
@dataclass
class Time(JsonSchemaMixin, Replaceable):
class Time(dbtClassMixin, Replaceable):
count: int
period: TimePeriod
@@ -149,19 +172,14 @@ class Time(JsonSchemaMixin, Replaceable):
return actual_age > difference
class FreshnessStatus(StrEnum):
Pass = 'pass'
Warn = 'warn'
Error = 'error'
@dataclass
class FreshnessThreshold(JsonSchemaMixin, Mergeable):
class FreshnessThreshold(dbtClassMixin, Mergeable):
warn_after: Optional[Time] = None
error_after: Optional[Time] = None
filter: Optional[str] = None
def status(self, age: float) -> FreshnessStatus:
def status(self, age: float) -> "dbt.contracts.results.FreshnessStatus":
from dbt.contracts.results import FreshnessStatus
if self.error_after and self.error_after.exceeded(age):
return FreshnessStatus.Error
elif self.warn_after and self.warn_after.exceeded(age):
@@ -174,32 +192,12 @@ class FreshnessThreshold(JsonSchemaMixin, Mergeable):
@dataclass
class AdditionalPropertiesAllowed(ExtensibleJsonSchemaMixin):
class AdditionalPropertiesAllowed(
AdditionalPropertiesMixin,
ExtensibleDbtClassMixin
):
_extra: Dict[str, Any] = field(default_factory=dict)
@property
def extra(self):
return self._extra
@classmethod
def from_dict(cls, data, validate=True):
self = super().from_dict(data=data, validate=validate)
keys = self.to_dict(validate=False, omit_none=False)
for key, value in data.items():
if key not in keys:
self._extra[key] = value
return self
def to_dict(self, omit_none=True, validate=False):
data = super().to_dict(omit_none=omit_none, validate=validate)
data.update(self._extra)
return data
def replace(self, **kwargs):
dct = self.to_dict(omit_none=False, validate=False)
dct.update(kwargs)
return self.from_dict(dct)
@dataclass
class ExternalPartition(AdditionalPropertiesAllowed, Replaceable):
@@ -228,7 +226,7 @@ class ExternalTable(AdditionalPropertiesAllowed, Mergeable):
@dataclass
class Quoting(JsonSchemaMixin, Mergeable):
class Quoting(dbtClassMixin, Mergeable):
database: Optional[bool] = None
schema: Optional[bool] = None
identifier: Optional[bool] = None
@@ -243,14 +241,18 @@ class UnparsedSourceTableDefinition(HasColumnTests, HasTests):
freshness: Optional[FreshnessThreshold] = field(
default_factory=FreshnessThreshold
)
external: Optional[ExternalTable] = field(
default_factory=ExternalTable
)
external: Optional[ExternalTable] = None
tags: List[str] = field(default_factory=list)
def __post_serialize__(self, dct):
dct = super().__post_serialize__(dct)
if 'freshness' not in dct and self.freshness is None:
dct['freshness'] = None
return dct
@dataclass
class UnparsedSourceDefinition(JsonSchemaMixin, Replaceable):
class UnparsedSourceDefinition(dbtClassMixin, Replaceable):
name: str
description: str = ''
meta: Dict[str, Any] = field(default_factory=dict)
@@ -264,20 +266,172 @@ class UnparsedSourceDefinition(JsonSchemaMixin, Replaceable):
loaded_at_field: Optional[str] = None
tables: List[UnparsedSourceTableDefinition] = field(default_factory=list)
tags: List[str] = field(default_factory=list)
config: Dict[str, Any] = field(default_factory=dict)
@property
def yaml_key(self) -> 'str':
return 'sources'
def __post_serialize__(self, dct):
dct = super().__post_serialize__(dct)
if 'freshnewss' not in dct and self.freshness is None:
dct['freshness'] = None
return dct
@dataclass
class UnparsedDocumentationFile(JsonSchemaMixin, Replaceable):
class SourceTablePatch(dbtClassMixin):
name: str
description: Optional[str] = None
meta: Optional[Dict[str, Any]] = None
data_type: Optional[str] = None
docs: Optional[Docs] = None
loaded_at_field: Optional[str] = None
identifier: Optional[str] = None
quoting: Quoting = field(default_factory=Quoting)
freshness: Optional[FreshnessThreshold] = field(
default_factory=FreshnessThreshold
)
external: Optional[ExternalTable] = None
tags: Optional[List[str]] = None
tests: Optional[List[TestDef]] = None
columns: Optional[Sequence[UnparsedColumn]] = None
def to_patch_dict(self) -> Dict[str, Any]:
dct = self.to_dict(omit_none=True)
remove_keys = ('name')
for key in remove_keys:
if key in dct:
del dct[key]
if self.freshness is None:
dct['freshness'] = None
return dct
@dataclass
class SourcePatch(dbtClassMixin, Replaceable):
name: str = field(
metadata=dict(description='The name of the source to override'),
)
overrides: str = field(
metadata=dict(description='The package of the source to override'),
)
path: Path = field(
metadata=dict(description='The path to the patch-defining yml file'),
)
description: Optional[str] = None
meta: Optional[Dict[str, Any]] = None
database: Optional[str] = None
schema: Optional[str] = None
loader: Optional[str] = None
quoting: Optional[Quoting] = None
freshness: Optional[Optional[FreshnessThreshold]] = field(
default_factory=FreshnessThreshold
)
loaded_at_field: Optional[str] = None
tables: Optional[List[SourceTablePatch]] = None
tags: Optional[List[str]] = None
def to_patch_dict(self) -> Dict[str, Any]:
dct = self.to_dict(omit_none=True)
remove_keys = ('name', 'overrides', 'tables', 'path')
for key in remove_keys:
if key in dct:
del dct[key]
if self.freshness is None:
dct['freshness'] = None
return dct
def get_table_named(self, name: str) -> Optional[SourceTablePatch]:
if self.tables is not None:
for table in self.tables:
if table.name == name:
return table
return None
@dataclass
class UnparsedDocumentation(dbtClassMixin, Replaceable):
package_name: str
root_path: str
path: str
original_file_path: str
file_contents: str
@property
def file_id(self):
return f'{self.package_name}://{self.original_file_path}'
@property
def resource_type(self):
return NodeType.Documentation
@dataclass
class UnparsedDocumentationFile(UnparsedDocumentation):
file_contents: str
# can't use total_ordering decorator here, as str provides an ordering already
# and it's not the one we want.
class Maturity(StrEnum):
low = 'low'
medium = 'medium'
high = 'high'
def __lt__(self, other):
if not isinstance(other, Maturity):
return NotImplemented
order = (Maturity.low, Maturity.medium, Maturity.high)
return order.index(self) < order.index(other)
def __gt__(self, other):
if not isinstance(other, Maturity):
return NotImplemented
return self != other and not (self < other)
def __ge__(self, other):
if not isinstance(other, Maturity):
return NotImplemented
return self == other or not (self < other)
def __le__(self, other):
if not isinstance(other, Maturity):
return NotImplemented
return self == other or self < other
class ExposureType(StrEnum):
Dashboard = 'dashboard'
Notebook = 'notebook'
Analysis = 'analysis'
ML = 'ml'
Application = 'application'
class MaturityType(StrEnum):
Low = 'low'
Medium = 'medium'
High = 'high'
@dataclass
class ExposureOwner(dbtClassMixin, Replaceable):
email: str
name: Optional[str] = None
@dataclass
class UnparsedExposure(dbtClassMixin, Replaceable):
name: str
type: ExposureType
owner: ExposureOwner
description: str = ''
maturity: Optional[MaturityType] = None
meta: Dict[str, Any] = field(default_factory=dict)
tags: List[str] = field(default_factory=list)
url: Optional[str] = None
depends_on: List[str] = field(default_factory=list)

View File

@@ -1,29 +1,42 @@
from dbt.contracts.util import Replaceable, Mergeable, list_str
from dbt.contracts.connection import UserConfigContract
from dbt.contracts.connection import UserConfigContract, QueryComment
from dbt.helper_types import NoValue
from dbt.logger import GLOBAL_LOGGER as logger # noqa
from dbt import tracking
from dbt.ui import printer
from dbt.helper_types import NoValue
from hologram import JsonSchemaMixin, ValidationError
from hologram.helpers import HyphenatedJsonSchemaMixin, register_pattern, \
ExtensibleJsonSchemaMixin
from dbt import ui
from dbt.dataclass_schema import (
dbtClassMixin, ValidationError,
HyphenatedDbtClassMixin,
ExtensibleDbtClassMixin,
register_pattern, ValidatedStringMixin
)
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Union, Any, NewType
from typing import Optional, List, Dict, Union, Any
from mashumaro.types import SerializableType
PIN_PACKAGE_URL = 'https://docs.getdbt.com/docs/package-management#section-specifying-package-versions' # noqa
PIN_PACKAGE_URL = 'https://docs.getdbt.com/docs/package-management#section-specifying-package-versions' # noqa
DEFAULT_SEND_ANONYMOUS_USAGE_STATS = True
DEFAULT_USE_COLORS = True
Name = NewType('Name', str)
class Name(ValidatedStringMixin):
ValidationRegex = r'^[^\d\W]\w*$'
register_pattern(Name, r'^[^\d\W]\w*$')
class SemverString(str, SerializableType):
def _serialize(self) -> str:
return self
@classmethod
def _deserialize(cls, value: str) -> 'SemverString':
return SemverString(value)
# this does not support the full semver (does not allow a trailing -fooXYZ) and
# is not restrictive enough for full semver, (allows '1.0'). But it's like
# 'semver lite'.
SemverString = NewType('SemverString', str)
register_pattern(
SemverString,
r'^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(\.(?:0|[1-9]\d*))?$',
@@ -31,15 +44,15 @@ register_pattern(
@dataclass
class Quoting(JsonSchemaMixin, Mergeable):
identifier: Optional[bool]
schema: Optional[bool]
database: Optional[bool]
project: Optional[bool]
class Quoting(dbtClassMixin, Mergeable):
schema: Optional[bool] = None
database: Optional[bool] = None
project: Optional[bool] = None
identifier: Optional[bool] = None
@dataclass
class Package(Replaceable, HyphenatedJsonSchemaMixin):
class Package(Replaceable, HyphenatedDbtClassMixin):
pass
@@ -48,24 +61,42 @@ class LocalPackage(Package):
local: str
# `float` also allows `int`, according to PEP484 (and jsonschema!)
RawVersion = Union[str, float]
@dataclass
class GitPackage(Package):
git: str
revision: Optional[str]
revision: Optional[RawVersion] = None
warn_unpinned: Optional[bool] = None
subdirectory: Optional[str] = None
def get_revisions(self) -> List[str]:
if self.revision is None:
return []
else:
return [str(self.revision)]
@dataclass
class RegistryPackage(Package):
package: str
version: Union[str, List[str]]
version: Union[RawVersion, List[RawVersion]]
install_prerelease: Optional[bool] = False
def get_versions(self) -> List[str]:
if isinstance(self.version, list):
return [str(v) for v in self.version]
else:
return [str(self.version)]
PackageSpec = Union[LocalPackage, GitPackage, RegistryPackage]
@dataclass
class PackageConfig(JsonSchemaMixin, Replaceable):
class PackageConfig(dbtClassMixin, Replaceable):
packages: List[PackageSpec]
@@ -81,13 +112,13 @@ class ProjectPackageMetadata:
@dataclass
class Downloads(ExtensibleJsonSchemaMixin, Replaceable):
class Downloads(ExtensibleDbtClassMixin, Replaceable):
tarball: str
@dataclass
class RegistryPackageMetadata(
ExtensibleJsonSchemaMixin,
ExtensibleDbtClassMixin,
ProjectPackageMetadata,
):
downloads: Downloads
@@ -127,6 +158,7 @@ BANNED_PROJECT_NAMES = {
'sql',
'sql_now',
'store_result',
'store_raw_result',
'target',
'this',
'tojson',
@@ -138,9 +170,10 @@ BANNED_PROJECT_NAMES = {
@dataclass
class Project(HyphenatedJsonSchemaMixin, Replaceable):
class Project(HyphenatedDbtClassMixin, Replaceable):
name: Name
version: Union[SemverString, float]
config_version: int
project_root: Optional[str] = None
source_paths: Optional[List[str]] = None
macro_paths: Optional[List[str]] = None
@@ -148,6 +181,7 @@ class Project(HyphenatedJsonSchemaMixin, Replaceable):
test_paths: Optional[List[str]] = None
analysis_paths: Optional[List[str]] = None
docs_paths: Optional[List[str]] = None
asset_paths: Optional[List[str]] = None
target_path: Optional[str] = None
snapshot_paths: Optional[List[str]] = None
clean_targets: Optional[List[str]] = None
@@ -158,27 +192,42 @@ class Project(HyphenatedJsonSchemaMixin, Replaceable):
on_run_start: Optional[List[str]] = field(default_factory=list_str)
on_run_end: Optional[List[str]] = field(default_factory=list_str)
require_dbt_version: Optional[Union[List[str], str]] = None
dispatch: List[Dict[str, Any]] = field(default_factory=list)
models: Dict[str, Any] = field(default_factory=dict)
seeds: Dict[str, Any] = field(default_factory=dict)
snapshots: Dict[str, Any] = field(default_factory=dict)
analyses: Dict[str, Any] = field(default_factory=dict)
sources: Dict[str, Any] = field(default_factory=dict)
tests: Dict[str, Any] = field(default_factory=dict)
vars: Optional[Dict[str, Any]] = field(
default=None,
metadata=dict(
description='map project names to their vars override dicts',
),
)
packages: List[PackageSpec] = field(default_factory=list)
query_comment: Optional[Union[str, NoValue]] = NoValue()
query_comment: Optional[Union[QueryComment, NoValue, str]] = NoValue()
@classmethod
def from_dict(cls, data, validate=True):
result = super().from_dict(data, validate=validate)
if result.name in BANNED_PROJECT_NAMES:
def validate(cls, data):
super().validate(data)
if data['name'] in BANNED_PROJECT_NAMES:
raise ValidationError(
'Invalid project name: {} is a reserved word'
.format(result.name)
f"Invalid project name: {data['name']} is a reserved word"
)
return result
# validate dispatch config
if 'dispatch' in data and data['dispatch']:
entries = data['dispatch']
for entry in entries:
if ('macro_namespace' not in entry or 'search_order' not in entry or
not isinstance(entry['search_order'], list)):
raise ValidationError(f"Invalid project dispatch config: {entry}")
@dataclass
class UserConfig(ExtensibleJsonSchemaMixin, Replaceable, UserConfigContract):
class UserConfig(ExtensibleDbtClassMixin, Replaceable, UserConfigContract):
send_anonymous_usage_stats: bool = DEFAULT_SEND_ANONYMOUS_USAGE_STATS
use_colors: bool = DEFAULT_USE_COLORS
use_colors: Optional[bool] = None
partial_parse: Optional[bool] = None
printer_width: Optional[int] = None
@@ -188,15 +237,15 @@ class UserConfig(ExtensibleJsonSchemaMixin, Replaceable, UserConfigContract):
else:
tracking.do_not_track()
if self.use_colors:
printer.use_colors()
if self.use_colors is not None:
ui.use_colors(self.use_colors)
if self.printer_width:
printer.printer_width(self.printer_width)
ui.printer_width(self.printer_width)
@dataclass
class ProfileConfig(HyphenatedJsonSchemaMixin, Replaceable):
class ProfileConfig(HyphenatedDbtClassMixin, Replaceable):
profile_name: str = field(metadata={'preserve_underscore': True})
target_name: str = field(metadata={'preserve_underscore': True})
config: UserConfig
@@ -207,10 +256,10 @@ class ProfileConfig(HyphenatedJsonSchemaMixin, Replaceable):
@dataclass
class ConfiguredQuoting(Quoting, Replaceable):
identifier: bool
schema: bool
database: Optional[bool]
project: Optional[bool]
identifier: bool = True
schema: bool = True
database: Optional[bool] = None
project: Optional[bool] = None
@dataclass
@@ -223,5 +272,5 @@ class Configuration(Project, ProfileConfig):
@dataclass
class ProjectList(JsonSchemaMixin):
class ProjectList(dbtClassMixin):
projects: Dict[str, Project]

View File

@@ -0,0 +1,130 @@
from collections.abc import Mapping
from dataclasses import dataclass, fields
from typing import (
Optional, Dict,
)
from typing_extensions import Protocol
from dbt.dataclass_schema import dbtClassMixin, StrEnum
from dbt import deprecations
from dbt.contracts.util import Replaceable
from dbt.exceptions import CompilationException
from dbt.utils import deep_merge
class RelationType(StrEnum):
Table = 'table'
View = 'view'
CTE = 'cte'
MaterializedView = 'materializedview'
External = 'external'
class ComponentName(StrEnum):
Database = 'database'
Schema = 'schema'
Identifier = 'identifier'
class HasQuoting(Protocol):
quoting: Dict[str, bool]
class FakeAPIObject(dbtClassMixin, Replaceable, Mapping):
# override the mapping truthiness, len is always >1
def __bool__(self):
return True
def __getitem__(self, key):
try:
return getattr(self, key)
except AttributeError:
raise KeyError(key) from None
def __iter__(self):
deprecations.warn('not-a-dictionary', obj=self)
for _, name in self._get_fields():
yield name
def __len__(self):
deprecations.warn('not-a-dictionary', obj=self)
return len(fields(self.__class__))
def incorporate(self, **kwargs):
value = self.to_dict(omit_none=True)
value = deep_merge(value, kwargs)
return self.from_dict(value)
@dataclass
class Policy(FakeAPIObject):
database: bool = True
schema: bool = True
identifier: bool = True
def get_part(self, key: ComponentName) -> bool:
if key == ComponentName.Database:
return self.database
elif key == ComponentName.Schema:
return self.schema
elif key == ComponentName.Identifier:
return self.identifier
else:
raise ValueError(
'Got a key of {}, expected one of {}'
.format(key, list(ComponentName))
)
def replace_dict(self, dct: Dict[ComponentName, bool]):
kwargs: Dict[str, bool] = {}
for k, v in dct.items():
kwargs[str(k)] = v
return self.replace(**kwargs)
@dataclass
class Path(FakeAPIObject):
database: Optional[str] = None
schema: Optional[str] = None
identifier: Optional[str] = None
def __post_init__(self):
# handle pesky jinja2.Undefined sneaking in here and messing up rende
if not isinstance(self.database, (type(None), str)):
raise CompilationException(
'Got an invalid path database: {}'.format(self.database)
)
if not isinstance(self.schema, (type(None), str)):
raise CompilationException(
'Got an invalid path schema: {}'.format(self.schema)
)
if not isinstance(self.identifier, (type(None), str)):
raise CompilationException(
'Got an invalid path identifier: {}'.format(self.identifier)
)
def get_lowered_part(self, key: ComponentName) -> Optional[str]:
part = self.get_part(key)
if part is not None:
part = part.lower()
return part
def get_part(self, key: ComponentName) -> Optional[str]:
if key == ComponentName.Database:
return self.database
elif key == ComponentName.Schema:
return self.schema
elif key == ComponentName.Identifier:
return self.identifier
else:
raise ValueError(
'Got a key of {}, expected one of {}'
.format(key, list(ComponentName))
)
def replace_dict(self, dct: Dict[ComponentName, str]):
kwargs: Dict[str, str] = {}
for k, v in dct.items():
kwargs[str(k)] = v
return self.replace(**kwargs)

View File

@@ -1,27 +1,37 @@
from dbt.contracts.graph.manifest import CompileResultNode
from dbt.contracts.graph.unparsed import (
Time, FreshnessStatus, FreshnessThreshold
FreshnessThreshold
)
from dbt.contracts.graph.parsed import ParsedSourceDefinition
from dbt.contracts.util import Writable, Replaceable
from dbt.contracts.util import (
BaseArtifactMetadata,
ArtifactMixin,
VersionedSchema,
Replaceable,
schema_version,
)
from dbt.exceptions import InternalException
from dbt.logger import (
TimingProcessor,
JsonOnly,
GLOBAL_LOGGER as logger,
)
from hologram.helpers import StrEnum
from hologram import JsonSchemaMixin
from dbt.utils import lowercase
from dbt.dataclass_schema import dbtClassMixin, StrEnum
import agate
from dataclasses import dataclass, field
from datetime import datetime
from typing import Union, Dict, List, Optional, Any, NamedTuple
from typing import (
Union, Dict, List, Optional, Any, NamedTuple, Sequence,
)
from dbt.clients.system import write_json
@dataclass
class TimingInfo(JsonSchemaMixin):
class TimingInfo(dbtClassMixin):
name: str
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
@@ -47,43 +57,78 @@ class collect_timing_info:
logger.debug('finished collecting timing info')
@dataclass
class PartialResult(JsonSchemaMixin, Writable):
node: CompileResultNode
error: Optional[str] = None
status: Union[None, str, int, bool] = None
execution_time: Union[str, int] = 0
thread_id: Optional[str] = None
timing: List[TimingInfo] = field(default_factory=list)
fail: Optional[bool] = None
warn: Optional[bool] = None
class NodeStatus(StrEnum):
Success = "success"
Error = "error"
Fail = "fail"
Warn = "warn"
Skipped = "skipped"
Pass = "pass"
RuntimeErr = "runtime error"
class RunStatus(StrEnum):
Success = NodeStatus.Success
Error = NodeStatus.Error
Skipped = NodeStatus.Skipped
class TestStatus(StrEnum):
Pass = NodeStatus.Pass
Error = NodeStatus.Error
Fail = NodeStatus.Fail
Warn = NodeStatus.Warn
Skipped = NodeStatus.Skipped
class FreshnessStatus(StrEnum):
Pass = NodeStatus.Pass
Warn = NodeStatus.Warn
Error = NodeStatus.Error
RuntimeErr = NodeStatus.RuntimeErr
@dataclass
class BaseResult(dbtClassMixin):
status: Union[RunStatus, TestStatus, FreshnessStatus]
timing: List[TimingInfo]
thread_id: str
execution_time: float
adapter_response: Dict[str, Any]
message: Optional[str]
failures: Optional[int]
@classmethod
def __pre_deserialize__(cls, data):
data = super().__pre_deserialize__(data)
if 'message' not in data:
data['message'] = None
if 'failures' not in data:
data['failures'] = None
return data
@dataclass
class NodeResult(BaseResult):
node: CompileResultNode
@dataclass
class RunResult(NodeResult):
agate_table: Optional[agate.Table] = field(
default=None, metadata={
'serialize': lambda x: None, 'deserialize': lambda x: None
}
)
# if the result got to the point where it could be skipped/failed, we would
# be returning a real result, not a partial.
@property
def skipped(self):
return False
return self.status == RunStatus.Skipped
@dataclass
class WritableRunModelResult(PartialResult):
skip: bool = False
@dataclass
class RunModelResult(WritableRunModelResult):
agate_table: Optional[agate.Table] = None
def to_dict(self, *args, **kwargs):
dct = super().to_dict(*args, **kwargs)
dct.pop('agate_table', None)
return dct
@dataclass
class ExecutionResult(JsonSchemaMixin, Writable):
results: List[Union[WritableRunModelResult, PartialResult]]
generated_at: datetime
class ExecutionResult(dbtClassMixin):
results: Sequence[BaseResult]
elapsed_time: float
def __len__(self):
@@ -96,183 +141,291 @@ class ExecutionResult(JsonSchemaMixin, Writable):
return self.results[idx]
@dataclass
class RunResultsMetadata(BaseArtifactMetadata):
dbt_schema_version: str = field(
default_factory=lambda: str(RunResultsArtifact.dbt_schema_version)
)
@dataclass
class RunResultOutput(BaseResult):
unique_id: str
def process_run_result(result: RunResult) -> RunResultOutput:
return RunResultOutput(
unique_id=result.node.unique_id,
status=result.status,
timing=result.timing,
thread_id=result.thread_id,
execution_time=result.execution_time,
message=result.message,
adapter_response=result.adapter_response,
failures=result.failures
)
@dataclass
class RunExecutionResult(
ExecutionResult,
):
results: Sequence[RunResult]
args: Dict[str, Any] = field(default_factory=dict)
generated_at: datetime = field(default_factory=datetime.utcnow)
def write(self, path: str):
writable = RunResultsArtifact.from_execution_results(
results=self.results,
elapsed_time=self.elapsed_time,
generated_at=self.generated_at,
args=self.args,
)
writable.write(path)
@dataclass
@schema_version('run-results', 2)
class RunResultsArtifact(ExecutionResult, ArtifactMixin):
results: Sequence[RunResultOutput]
args: Dict[str, Any] = field(default_factory=dict)
@classmethod
def from_execution_results(
cls,
results: Sequence[RunResult],
elapsed_time: float,
generated_at: datetime,
args: Dict,
):
processed_results = [process_run_result(result) for result in results]
meta = RunResultsMetadata(
dbt_schema_version=str(cls.dbt_schema_version),
generated_at=generated_at,
)
return cls(
metadata=meta,
results=processed_results,
elapsed_time=elapsed_time,
args=args
)
def write(self, path: str):
write_json(path, self.to_dict(omit_none=False))
@dataclass
class RunOperationResult(ExecutionResult):
success: bool
@dataclass
class RunOperationResultMetadata(BaseArtifactMetadata):
dbt_schema_version: str = field(default_factory=lambda: str(
RunOperationResultsArtifact.dbt_schema_version
))
@dataclass
@schema_version('run-operation-result', 1)
class RunOperationResultsArtifact(RunOperationResult, ArtifactMixin):
@classmethod
def from_success(
cls,
success: bool,
elapsed_time: float,
generated_at: datetime,
):
meta = RunOperationResultMetadata(
dbt_schema_version=str(cls.dbt_schema_version),
generated_at=generated_at,
)
return cls(
metadata=meta,
results=[],
elapsed_time=elapsed_time,
success=success,
)
# due to issues with typing.Union collapsing subclasses, this can't subclass
# PartialResult
@dataclass
class SourceFreshnessResult(JsonSchemaMixin, Writable):
class SourceFreshnessResult(NodeResult):
node: ParsedSourceDefinition
status: FreshnessStatus
max_loaded_at: datetime
snapshotted_at: datetime
age: float
status: FreshnessStatus
error: Optional[str] = None
execution_time: Union[str, int] = 0
thread_id: Optional[str] = None
timing: List[TimingInfo] = field(default_factory=list)
fail: Optional[bool] = None
def __post_init__(self):
self.fail = self.status == 'error'
@property
def warned(self):
return self.status == 'warn'
@property
def skipped(self):
return False
@dataclass
class FreshnessMetadata(JsonSchemaMixin):
generated_at: datetime
elapsed_time: float
@dataclass
class FreshnessExecutionResult(FreshnessMetadata):
results: List[Union[PartialResult, SourceFreshnessResult]]
def write(self, path, omit_none=True):
"""Create a new object with the desired output schema and write it."""
meta = FreshnessMetadata(
generated_at=self.generated_at,
elapsed_time=self.elapsed_time,
)
sources = {}
for result in self.results:
result_value: Union[
SourceFreshnessRuntimeError, SourceFreshnessOutput
]
unique_id = result.node.unique_id
if result.error is not None:
result_value = SourceFreshnessRuntimeError(
error=result.error,
state=FreshnessErrorEnum.runtime_error,
)
else:
# we know that this must be a SourceFreshnessResult
if not isinstance(result, SourceFreshnessResult):
raise InternalException(
'Got {} instead of a SourceFreshnessResult for a '
'non-error result in freshness execution!'
.format(type(result))
)
# if we're here, we must have a non-None freshness threshold
criteria = result.node.freshness
if criteria is None:
raise InternalException(
'Somehow evaluated a freshness result for a source '
'that has no freshness criteria!'
)
result_value = SourceFreshnessOutput(
max_loaded_at=result.max_loaded_at,
snapshotted_at=result.snapshotted_at,
max_loaded_at_time_ago_in_s=result.age,
state=result.status,
criteria=criteria,
)
sources[unique_id] = result_value
output = FreshnessRunOutput(meta=meta, sources=sources)
output.write(path, omit_none=omit_none)
def __len__(self):
return len(self.results)
def __iter__(self):
return iter(self.results)
def __getitem__(self, idx):
return self.results[idx]
def _copykeys(src, keys, **updates):
return {k: getattr(src, k) for k in keys}
@dataclass
class FreshnessCriteria(JsonSchemaMixin):
warn_after: Time
error_after: Time
class FreshnessErrorEnum(StrEnum):
runtime_error = 'runtime error'
@dataclass
class SourceFreshnessRuntimeError(JsonSchemaMixin):
error: str
state: FreshnessErrorEnum
class SourceFreshnessRuntimeError(dbtClassMixin):
unique_id: str
error: Optional[Union[str, int]]
status: FreshnessErrorEnum
@dataclass
class SourceFreshnessOutput(JsonSchemaMixin):
class SourceFreshnessOutput(dbtClassMixin):
unique_id: str
max_loaded_at: datetime
snapshotted_at: datetime
max_loaded_at_time_ago_in_s: float
state: FreshnessStatus
status: FreshnessStatus
criteria: FreshnessThreshold
SourceFreshnessRunResult = Union[SourceFreshnessOutput,
SourceFreshnessRuntimeError]
adapter_response: Dict[str, Any]
@dataclass
class FreshnessRunOutput(JsonSchemaMixin, Writable):
meta: FreshnessMetadata
sources: Dict[str, SourceFreshnessRunResult]
class PartialSourceFreshnessResult(NodeResult):
status: FreshnessStatus
@property
def skipped(self):
return False
FreshnessNodeResult = Union[PartialSourceFreshnessResult,
SourceFreshnessResult]
FreshnessNodeOutput = Union[SourceFreshnessRuntimeError, SourceFreshnessOutput]
def process_freshness_result(
result: FreshnessNodeResult
) -> FreshnessNodeOutput:
unique_id = result.node.unique_id
if result.status == FreshnessStatus.RuntimeErr:
return SourceFreshnessRuntimeError(
unique_id=unique_id,
error=result.message,
status=FreshnessErrorEnum.runtime_error,
)
# we know that this must be a SourceFreshnessResult
if not isinstance(result, SourceFreshnessResult):
raise InternalException(
'Got {} instead of a SourceFreshnessResult for a '
'non-error result in freshness execution!'
.format(type(result))
)
# if we're here, we must have a non-None freshness threshold
criteria = result.node.freshness
if criteria is None:
raise InternalException(
'Somehow evaluated a freshness result for a source '
'that has no freshness criteria!'
)
return SourceFreshnessOutput(
unique_id=unique_id,
max_loaded_at=result.max_loaded_at,
snapshotted_at=result.snapshotted_at,
max_loaded_at_time_ago_in_s=result.age,
status=result.status,
criteria=criteria,
adapter_response=result.adapter_response
)
@dataclass
class FreshnessMetadata(BaseArtifactMetadata):
dbt_schema_version: str = field(
default_factory=lambda: str(
FreshnessExecutionResultArtifact.dbt_schema_version
)
)
@dataclass
class FreshnessResult(ExecutionResult):
metadata: FreshnessMetadata
results: Sequence[FreshnessNodeResult]
@classmethod
def from_node_results(
cls,
results: List[FreshnessNodeResult],
elapsed_time: float,
generated_at: datetime,
):
meta = FreshnessMetadata(generated_at=generated_at)
return cls(metadata=meta, results=results, elapsed_time=elapsed_time)
@dataclass
@schema_version('sources', 1)
class FreshnessExecutionResultArtifact(
ArtifactMixin,
VersionedSchema,
):
metadata: FreshnessMetadata
results: Sequence[FreshnessNodeOutput]
elapsed_time: float
@classmethod
def from_result(cls, base: FreshnessResult):
processed = [process_freshness_result(r) for r in base.results]
return cls(
metadata=base.metadata,
results=processed,
elapsed_time=base.elapsed_time,
)
Primitive = Union[bool, str, float, None]
PrimitiveDict = Dict[str, Primitive]
CatalogKey = NamedTuple(
'CatalogKey',
[('database', str), ('schema', str), ('name', str)]
[('database', Optional[str]), ('schema', str), ('name', str)]
)
@dataclass
class StatsItem(JsonSchemaMixin):
class StatsItem(dbtClassMixin):
id: str
label: str
value: Primitive
description: str
include: bool
description: Optional[str] = None
StatsDict = Dict[str, StatsItem]
@dataclass
class ColumnMetadata(JsonSchemaMixin):
class ColumnMetadata(dbtClassMixin):
type: str
comment: Optional[str]
index: int
name: str
comment: Optional[str] = None
ColumnMap = Dict[str, ColumnMetadata]
@dataclass
class TableMetadata(JsonSchemaMixin):
class TableMetadata(dbtClassMixin):
type: str
database: str
schema: str
name: str
comment: Optional[str]
owner: Optional[str]
database: Optional[str] = None
comment: Optional[str] = None
owner: Optional[str] = None
@dataclass
class CatalogTable(JsonSchemaMixin, Replaceable):
class CatalogTable(dbtClassMixin, Replaceable):
metadata: TableMetadata
columns: ColumnMap
stats: StatsDict
@@ -281,15 +434,52 @@ class CatalogTable(JsonSchemaMixin, Replaceable):
def key(self) -> CatalogKey:
return CatalogKey(
self.metadata.database.lower(),
lowercase(self.metadata.database),
self.metadata.schema.lower(),
self.metadata.name.lower(),
)
@dataclass
class CatalogResults(JsonSchemaMixin, Writable):
class CatalogMetadata(BaseArtifactMetadata):
dbt_schema_version: str = field(
default_factory=lambda: str(CatalogArtifact.dbt_schema_version)
)
@dataclass
class CatalogResults(dbtClassMixin):
nodes: Dict[str, CatalogTable]
generated_at: datetime
errors: Optional[List[str]]
sources: Dict[str, CatalogTable]
errors: Optional[List[str]] = None
_compile_results: Optional[Any] = None
def __post_serialize__(self, dct):
dct = super().__post_serialize__(dct)
if '_compile_results' in dct:
del dct['_compile_results']
return dct
@dataclass
@schema_version('catalog', 1)
class CatalogArtifact(CatalogResults, ArtifactMixin):
metadata: CatalogMetadata
@classmethod
def from_results(
cls,
generated_at: datetime,
nodes: Dict[str, CatalogTable],
sources: Dict[str, CatalogTable],
compile_results: Optional[Any],
errors: Optional[List[str]]
) -> 'CatalogArtifact':
meta = CatalogMetadata(generated_at=generated_at)
return cls(
metadata=meta,
nodes=nodes,
sources=sources,
errors=errors,
_compile_results=compile_results,
)

View File

@@ -3,17 +3,24 @@ import os
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional, Union, List, Any, Dict, Type
from typing import Optional, Union, List, Any, Dict, Type, Sequence
from hologram import JsonSchemaMixin
from hologram.helpers import StrEnum
from dbt.dataclass_schema import dbtClassMixin, StrEnum
from dbt.contracts.graph.compiled import CompileResultNode
from dbt.contracts.graph.manifest import WritableManifest
from dbt.contracts.results import (
TimingInfo,
RunResult, RunResultsArtifact, TimingInfo,
CatalogArtifact,
CatalogResults,
ExecutionResult,
FreshnessExecutionResultArtifact,
FreshnessResult,
RunOperationResult,
RunOperationResultsArtifact,
RunExecutionResult,
)
from dbt.contracts.util import VersionedSchema, schema_version
from dbt.exceptions import InternalException
from dbt.logger import LogMessage
from dbt.utils import restrict_to
@@ -26,23 +33,57 @@ TaskID = uuid.UUID
@dataclass
class RPCParameters(JsonSchemaMixin):
timeout: Optional[float]
class RPCParameters(dbtClassMixin):
task_tags: TaskTags
timeout: Optional[float]
@classmethod
def __pre_deserialize__(cls, data, omit_none=True):
data = super().__pre_deserialize__(data)
if 'timeout' not in data:
data['timeout'] = None
if 'task_tags' not in data:
data['task_tags'] = None
return data
@dataclass
class RPCExecParameters(RPCParameters):
name: str
sql: str
macros: Optional[str]
macros: Optional[str] = None
@dataclass
class RPCCompileParameters(RPCParameters):
threads: Optional[int] = None
models: Union[None, str, List[str]] = None
select: Union[None, str, List[str]] = None
exclude: Union[None, str, List[str]] = None
selector: Optional[str] = None
state: Optional[str] = None
@dataclass
class RPCListParameters(RPCParameters):
resource_types: Optional[List[str]] = None
models: Union[None, str, List[str]] = None
exclude: Union[None, str, List[str]] = None
select: Union[None, str, List[str]] = None
selector: Optional[str] = None
output: Optional[str] = 'json'
output_keys: Optional[List[str]] = None
@dataclass
class RPCRunParameters(RPCParameters):
threads: Optional[int] = None
models: Union[None, str, List[str]] = None
select: Union[None, str, List[str]] = None
exclude: Union[None, str, List[str]] = None
selector: Optional[str] = None
state: Optional[str] = None
defer: Optional[bool] = None
@dataclass
@@ -50,12 +91,16 @@ class RPCSnapshotParameters(RPCParameters):
threads: Optional[int] = None
select: Union[None, str, List[str]] = None
exclude: Union[None, str, List[str]] = None
selector: Optional[str] = None
state: Optional[str] = None
@dataclass
class RPCTestParameters(RPCCompileParameters):
data: bool = False
schema: bool = False
state: Optional[str] = None
defer: Optional[bool] = None
@dataclass
@@ -63,12 +108,26 @@ class RPCSeedParameters(RPCParameters):
threads: Optional[int] = None
select: Union[None, str, List[str]] = None
exclude: Union[None, str, List[str]] = None
selector: Optional[str] = None
show: bool = False
state: Optional[str] = None
@dataclass
class RPCDocsGenerateParameters(RPCParameters):
compile: bool = True
state: Optional[str] = None
@dataclass
class RPCBuildParameters(RPCParameters):
threads: Optional[int] = None
models: Union[None, str, List[str]] = None
select: Union[None, str, List[str]] = None
exclude: Union[None, str, List[str]] = None
selector: Optional[str] = None
state: Optional[str] = None
defer: Optional[bool] = None
@dataclass
@@ -77,7 +136,7 @@ class RPCCliParameters(RPCParameters):
@dataclass
class RPCNoParameters(RPCParameters):
class RPCDepsParameters(RPCParameters):
pass
@@ -105,7 +164,7 @@ class StatusParameters(RPCParameters):
@dataclass
class GCSettings(JsonSchemaMixin):
class GCSettings(dbtClassMixin):
# start evicting the longest-ago-ended tasks here
maxsize: int
# start evicting all tasks before now - auto_reap_age when we have this
@@ -141,70 +200,175 @@ class RPCRunOperationParameters(RPCParameters):
class RPCSourceFreshnessParameters(RPCParameters):
threads: Optional[int] = None
select: Union[None, str, List[str]] = None
exclude: Union[None, str, List[str]] = None
selector: Optional[str] = None
@dataclass
class GetManifestParameters(RPCParameters):
pass
# Outputs
@dataclass
class RemoteResult(JsonSchemaMixin):
class RemoteResult(VersionedSchema):
logs: List[LogMessage]
@dataclass
class RemoteEmptyResult(RemoteResult):
pass
@schema_version('remote-list-results', 1)
class RemoteListResults(RemoteResult):
output: List[Any]
generated_at: datetime = field(default_factory=datetime.utcnow)
@dataclass
@schema_version('remote-deps-result', 1)
class RemoteDepsResult(RemoteResult):
generated_at: datetime = field(default_factory=datetime.utcnow)
@dataclass
@schema_version('remote-catalog-result', 1)
class RemoteCatalogResults(CatalogResults, RemoteResult):
pass
generated_at: datetime = field(default_factory=datetime.utcnow)
def write(self, path: str):
artifact = CatalogArtifact.from_results(
generated_at=self.generated_at,
nodes=self.nodes,
sources=self.sources,
compile_results=self._compile_results,
errors=self.errors,
)
artifact.write(path)
@dataclass
class RemoteCompileResult(RemoteResult):
class RemoteCompileResultMixin(RemoteResult):
raw_sql: str
compiled_sql: str
node: CompileResultNode
timing: List[TimingInfo]
@dataclass
@schema_version('remote-compile-result', 1)
class RemoteCompileResult(RemoteCompileResultMixin):
generated_at: datetime = field(default_factory=datetime.utcnow)
@property
def error(self):
return None
@dataclass
@schema_version('remote-execution-result', 1)
class RemoteExecutionResult(ExecutionResult, RemoteResult):
pass
results: Sequence[RunResult]
args: Dict[str, Any] = field(default_factory=dict)
generated_at: datetime = field(default_factory=datetime.utcnow)
def write(self, path: str):
writable = RunResultsArtifact.from_execution_results(
generated_at=self.generated_at,
results=self.results,
elapsed_time=self.elapsed_time,
args=self.args,
)
writable.write(path)
@classmethod
def from_local_result(
cls,
base: RunExecutionResult,
logs: List[LogMessage],
) -> 'RemoteExecutionResult':
return cls(
generated_at=base.generated_at,
results=base.results,
elapsed_time=base.elapsed_time,
args=base.args,
logs=logs,
)
@dataclass
class ResultTable(JsonSchemaMixin):
class ResultTable(dbtClassMixin):
column_names: List[str]
rows: List[Any]
@dataclass
class RemoteRunOperationResult(ExecutionResult, RemoteResult):
success: bool
@schema_version('remote-run-operation-result', 1)
class RemoteRunOperationResult(RunOperationResult, RemoteResult):
generated_at: datetime = field(default_factory=datetime.utcnow)
@classmethod
def from_local_result(
cls,
base: RunOperationResultsArtifact,
logs: List[LogMessage],
) -> 'RemoteRunOperationResult':
return cls(
generated_at=base.metadata.generated_at,
results=base.results,
elapsed_time=base.elapsed_time,
success=base.success,
logs=logs,
)
def write(self, path: str):
writable = RunOperationResultsArtifact.from_success(
success=self.success,
generated_at=self.generated_at,
elapsed_time=self.elapsed_time,
)
writable.write(path)
@dataclass
class RemoteRunResult(RemoteCompileResult):
@schema_version('remote-freshness-result', 1)
class RemoteFreshnessResult(FreshnessResult, RemoteResult):
@classmethod
def from_local_result(
cls,
base: FreshnessResult,
logs: List[LogMessage],
) -> 'RemoteFreshnessResult':
return cls(
metadata=base.metadata,
results=base.results,
elapsed_time=base.elapsed_time,
logs=logs,
)
def write(self, path: str):
writable = FreshnessExecutionResultArtifact.from_result(base=self)
writable.write(path)
@dataclass
@schema_version('remote-run-result', 1)
class RemoteRunResult(RemoteCompileResultMixin):
table: ResultTable
generated_at: datetime = field(default_factory=datetime.utcnow)
RPCResult = Union[
RemoteCompileResult,
RemoteExecutionResult,
RemoteFreshnessResult,
RemoteCatalogResults,
RemoteEmptyResult,
RemoteDepsResult,
RemoteRunOperationResult,
]
# GC types
class GCResultState(StrEnum):
Deleted = 'deleted' # successful GC
Missing = 'missing' # nothing to GC
@@ -212,6 +376,7 @@ class GCResultState(StrEnum):
@dataclass
@schema_version('remote-gc-result', 1)
class GCResult(RemoteResult):
logs: List[LogMessage] = field(default_factory=list)
deleted: List[TaskID] = field(default_factory=list)
@@ -287,24 +452,35 @@ class TaskHandlerState(StrEnum):
@dataclass
class TaskTiming(JsonSchemaMixin):
class TaskTiming(dbtClassMixin):
state: TaskHandlerState
start: Optional[datetime]
end: Optional[datetime]
elapsed: Optional[float]
# These ought to be defaults but superclass order doesn't
# allow that to work
@classmethod
def __pre_deserialize__(cls, data):
data = super().__pre_deserialize__(data)
for field_name in ('start', 'end', 'elapsed'):
if field_name not in data:
data[field_name] = None
return data
@dataclass
class TaskRow(TaskTiming):
task_id: TaskID
request_id: Union[str, int]
request_source: str
method: str
timeout: Optional[float]
tags: TaskTags
request_id: Union[str, int]
tags: TaskTags = None
timeout: Optional[float] = None
@dataclass
@schema_version('remote-ps-result', 1)
class PSResult(RemoteResult):
rows: List[TaskRow]
@@ -317,11 +493,18 @@ class KillResultStatus(StrEnum):
@dataclass
@schema_version('remote-kill-result', 1)
class KillResult(RemoteResult):
state: KillResultStatus = KillResultStatus.Missing
logs: List[LogMessage] = field(default_factory=list)
@dataclass
@schema_version('remote-manifest-result', 1)
class GetManifestResult(RemoteResult):
manifest: Optional[WritableManifest] = None
# this is kind of carefuly structured: BlocksManifestTasks is implied by
# RequiresConfigReloadBefore and RequiresManifestReloadAfter
class RemoteMethodFlags(enum.Flag):
@@ -343,18 +526,30 @@ class PollResult(RemoteResult, TaskTiming):
end: Optional[datetime]
elapsed: Optional[float]
# These ought to be defaults but superclass order doesn't
# allow that to work
@classmethod
def __pre_deserialize__(cls, data):
data = super().__pre_deserialize__(data)
for field_name in ('start', 'end', 'elapsed'):
if field_name not in data:
data[field_name] = None
return data
@dataclass
class PollRemoteEmptyCompleteResult(PollResult, RemoteEmptyResult):
@schema_version('poll-remote-deps-result', 1)
class PollRemoteEmptyCompleteResult(PollResult, RemoteResult):
state: TaskHandlerState = field(
metadata=restrict_to(TaskHandlerState.Success,
TaskHandlerState.Failed),
)
generated_at: datetime = field(default_factory=datetime.utcnow)
@classmethod
def from_result(
cls: Type['PollRemoteEmptyCompleteResult'],
base: RemoteEmptyResult,
base: RemoteDepsResult,
tags: TaskTags,
timing: TaskTiming,
logs: List[LogMessage],
@@ -366,10 +561,12 @@ class PollRemoteEmptyCompleteResult(PollResult, RemoteEmptyResult):
start=timing.start,
end=timing.end,
elapsed=timing.elapsed,
generated_at=base.generated_at
)
@dataclass
@schema_version('poll-remote-killed-result', 1)
class PollKilledResult(PollResult):
state: TaskHandlerState = field(
metadata=restrict_to(TaskHandlerState.Killed),
@@ -377,7 +574,11 @@ class PollKilledResult(PollResult):
@dataclass
class PollExecuteCompleteResult(RemoteExecutionResult, PollResult):
@schema_version('poll-remote-execution-result', 1)
class PollExecuteCompleteResult(
RemoteExecutionResult,
PollResult,
):
state: TaskHandlerState = field(
metadata=restrict_to(TaskHandlerState.Success,
TaskHandlerState.Failed),
@@ -393,7 +594,6 @@ class PollExecuteCompleteResult(RemoteExecutionResult, PollResult):
) -> 'PollExecuteCompleteResult':
return cls(
results=base.results,
generated_at=base.generated_at,
elapsed_time=base.elapsed_time,
logs=logs,
tags=tags,
@@ -401,11 +601,16 @@ class PollExecuteCompleteResult(RemoteExecutionResult, PollResult):
start=timing.start,
end=timing.end,
elapsed=timing.elapsed,
generated_at=base.generated_at,
)
@dataclass
class PollCompileCompleteResult(RemoteCompileResult, PollResult):
@schema_version('poll-remote-compile-result', 1)
class PollCompileCompleteResult(
RemoteCompileResult,
PollResult,
):
state: TaskHandlerState = field(
metadata=restrict_to(TaskHandlerState.Success,
TaskHandlerState.Failed),
@@ -430,11 +635,16 @@ class PollCompileCompleteResult(RemoteCompileResult, PollResult):
start=timing.start,
end=timing.end,
elapsed=timing.elapsed,
generated_at=base.generated_at
)
@dataclass
class PollRunCompleteResult(RemoteRunResult, PollResult):
@schema_version('poll-remote-run-result', 1)
class PollRunCompleteResult(
RemoteRunResult,
PollResult,
):
state: TaskHandlerState = field(
metadata=restrict_to(TaskHandlerState.Success,
TaskHandlerState.Failed),
@@ -460,11 +670,16 @@ class PollRunCompleteResult(RemoteRunResult, PollResult):
start=timing.start,
end=timing.end,
elapsed=timing.elapsed,
generated_at=base.generated_at
)
@dataclass
class PollRunOperationCompleteResult(RemoteRunOperationResult, PollResult):
@schema_version('poll-remote-run-operation-result', 1)
class PollRunOperationCompleteResult(
RemoteRunOperationResult,
PollResult,
):
state: TaskHandlerState = field(
metadata=restrict_to(TaskHandlerState.Success,
TaskHandlerState.Failed),
@@ -493,6 +708,7 @@ class PollRunOperationCompleteResult(RemoteRunOperationResult, PollResult):
@dataclass
@schema_version('poll-remote-catalog-result', 1)
class PollCatalogCompleteResult(RemoteCatalogResults, PollResult):
state: TaskHandlerState = field(
metadata=restrict_to(TaskHandlerState.Success,
@@ -509,6 +725,7 @@ class PollCatalogCompleteResult(RemoteCatalogResults, PollResult):
) -> 'PollCatalogCompleteResult':
return cls(
nodes=base.nodes,
sources=base.sources,
generated_at=base.generated_at,
errors=base.errors,
_compile_results=base._compile_results,
@@ -522,12 +739,69 @@ class PollCatalogCompleteResult(RemoteCatalogResults, PollResult):
@dataclass
@schema_version('poll-remote-in-progress-result', 1)
class PollInProgressResult(PollResult):
pass
@dataclass
@schema_version('poll-remote-get-manifest-result', 1)
class PollGetManifestResult(GetManifestResult, PollResult):
state: TaskHandlerState = field(
metadata=restrict_to(TaskHandlerState.Success,
TaskHandlerState.Failed),
)
@classmethod
def from_result(
cls: Type['PollGetManifestResult'],
base: GetManifestResult,
tags: TaskTags,
timing: TaskTiming,
logs: List[LogMessage],
) -> 'PollGetManifestResult':
return cls(
manifest=base.manifest,
logs=logs,
tags=tags,
state=timing.state,
start=timing.start,
end=timing.end,
elapsed=timing.elapsed,
)
@dataclass
@schema_version('poll-remote-freshness-result', 1)
class PollFreshnessResult(RemoteFreshnessResult, PollResult):
state: TaskHandlerState = field(
metadata=restrict_to(TaskHandlerState.Success,
TaskHandlerState.Failed),
)
@classmethod
def from_result(
cls: Type['PollFreshnessResult'],
base: RemoteFreshnessResult,
tags: TaskTags,
timing: TaskTiming,
logs: List[LogMessage],
) -> 'PollFreshnessResult':
return cls(
logs=logs,
tags=tags,
state=timing.state,
start=timing.start,
end=timing.end,
elapsed=timing.elapsed,
metadata=base.metadata,
results=base.results,
elapsed_time=base.elapsed_time,
)
# Manifest parsing types
class ManifestStatus(StrEnum):
Init = 'init'
Compiling = 'compiling'
@@ -536,6 +810,7 @@ class ManifestStatus(StrEnum):
@dataclass
@schema_version('remote-status-result', 1)
class LastParse(RemoteResult):
state: ManifestStatus = ManifestStatus.Init
logs: List[LogMessage] = field(default_factory=list)

View File

@@ -0,0 +1,22 @@
from dataclasses import dataclass
from dbt.dataclass_schema import dbtClassMixin
from typing import List, Dict, Any, Union
@dataclass
class SelectorDefinition(dbtClassMixin):
name: str
definition: Union[str, Dict[str, Any]]
description: str = ''
@dataclass
class SelectorFile(dbtClassMixin):
selectors: List[SelectorDefinition]
version: int = 2
# @dataclass
# class SelectorCollection:
# packages: Dict[str, List[SelectorFile]] = field(default_factory=dict)

View File

@@ -0,0 +1,18 @@
from pathlib import Path
from .graph.manifest import WritableManifest
from typing import Optional
from dbt.exceptions import IncompatibleSchemaException
class PreviousState:
def __init__(self, path: Path):
self.path: Path = path
self.manifest: Optional[WritableManifest] = None
manifest_path = self.path / 'manifest.json'
if manifest_path.exists() and manifest_path.is_file():
try:
self.manifest = WritableManifest.read(str(manifest_path))
except IncompatibleSchemaException as exc:
exc.add_filename(str(manifest_path))
raise

View File

@@ -1,11 +1,24 @@
import dataclasses
from typing import List
import os
from datetime import datetime
from typing import (
List, Tuple, ClassVar, Type, TypeVar, Dict, Any, Optional
)
from dbt.clients.system import write_json
from dbt.clients.system import write_json, read_json
from dbt.exceptions import (
InternalException,
RuntimeException,
)
from dbt.version import __version__
from dbt.tracking import get_invocation_id
from dbt.dataclass_schema import dbtClassMixin
SourceKey = Tuple[str, str]
def list_str() -> List[str]:
"""Mypy gets upset about¸stuff like:
"""Mypy gets upset about stuff like:
from dataclasses import dataclass, field
from typing import Optional, List
@@ -42,5 +55,158 @@ class Mergeable(Replaceable):
class Writable:
def write(self, path: str, omit_none: bool = False):
write_json(path, self.to_dict(omit_none=omit_none)) # type: ignore
def write(self, path: str):
write_json(
path, self.to_dict(omit_none=False) # type: ignore
)
class AdditionalPropertiesMixin:
"""Make this class an extensible property.
The underlying class definition must include a type definition for a field
named '_extra' that is of type `Dict[str, Any]`.
"""
ADDITIONAL_PROPERTIES = True
# This takes attributes in the dictionary that are
# not in the class definitions and puts them in an
# _extra dict in the class
@classmethod
def __pre_deserialize__(cls, data):
# dir() did not work because fields with
# metadata settings are not found
# The original version of this would create the
# object first and then update extra with the
# extra keys, but that won't work here, so
# we're copying the dict so we don't insert the
# _extra in the original data. This also requires
# that Mashumaro actually build the '_extra' field
cls_keys = cls._get_field_names()
new_dict = {}
for key, value in data.items():
if key not in cls_keys and key != '_extra':
if '_extra' not in new_dict:
new_dict['_extra'] = {}
new_dict['_extra'][key] = value
else:
new_dict[key] = value
data = new_dict
data = super().__pre_deserialize__(data)
return data
def __post_serialize__(self, dct):
data = super().__post_serialize__(dct)
data.update(self.extra)
if '_extra' in data:
del data['_extra']
return data
def replace(self, **kwargs):
dct = self.to_dict(omit_none=False)
dct.update(kwargs)
return self.from_dict(dct)
@property
def extra(self):
return self._extra
class Readable:
@classmethod
def read(cls, path: str):
try:
data = read_json(path)
except (EnvironmentError, ValueError) as exc:
raise RuntimeException(
f'Could not read {cls.__name__} at "{path}" as JSON: {exc}'
) from exc
return cls.from_dict(data) # type: ignore
BASE_SCHEMAS_URL = 'https://schemas.getdbt.com/'
SCHEMA_PATH = 'dbt/{name}/v{version}.json'
@dataclasses.dataclass
class SchemaVersion:
name: str
version: int
@property
def path(self) -> str:
return SCHEMA_PATH.format(
name=self.name,
version=self.version
)
def __str__(self) -> str:
return BASE_SCHEMAS_URL + self.path
SCHEMA_VERSION_KEY = 'dbt_schema_version'
METADATA_ENV_PREFIX = 'DBT_ENV_CUSTOM_ENV_'
def get_metadata_env() -> Dict[str, str]:
return {
k[len(METADATA_ENV_PREFIX):]: v for k, v in os.environ.items()
if k.startswith(METADATA_ENV_PREFIX)
}
@dataclasses.dataclass
class BaseArtifactMetadata(dbtClassMixin):
dbt_schema_version: str
dbt_version: str = __version__
generated_at: datetime = dataclasses.field(
default_factory=datetime.utcnow
)
invocation_id: Optional[str] = dataclasses.field(
default_factory=get_invocation_id
)
env: Dict[str, str] = dataclasses.field(default_factory=get_metadata_env)
def schema_version(name: str, version: int):
def inner(cls: Type[VersionedSchema]):
cls.dbt_schema_version = SchemaVersion(
name=name,
version=version,
)
return cls
return inner
@dataclasses.dataclass
class VersionedSchema(dbtClassMixin):
dbt_schema_version: ClassVar[SchemaVersion]
@classmethod
def json_schema(cls, embeddable: bool = False) -> Dict[str, Any]:
result = super().json_schema(embeddable=embeddable)
if not embeddable:
result['$id'] = str(cls.dbt_schema_version)
return result
T = TypeVar('T', bound='ArtifactMixin')
# metadata should really be a Generic[T_M] where T_M is a TypeVar bound to
# BaseArtifactMetadata. Unfortunately this isn't possible due to a mypy issue:
# https://github.com/python/mypy/issues/7520
@dataclasses.dataclass(init=False)
class ArtifactMixin(VersionedSchema, Writable, Readable):
metadata: BaseArtifactMetadata
@classmethod
def validate(cls, data):
super().validate(data)
if cls.dbt_schema_version is None:
raise InternalException(
'Cannot call from_dict with no schema version!'
)

View File

@@ -0,0 +1,169 @@
from typing import (
Type, ClassVar, cast,
)
import re
from dataclasses import fields
from enum import Enum
from datetime import datetime
from dateutil.parser import parse
from hologram import JsonSchemaMixin, FieldEncoder, ValidationError
# type: ignore
from mashumaro import DataClassDictMixin
from mashumaro.config import (
TO_DICT_ADD_OMIT_NONE_FLAG, BaseConfig as MashBaseConfig
)
from mashumaro.types import SerializableType, SerializationStrategy
class DateTimeSerialization(SerializationStrategy):
def serialize(self, value):
out = value.isoformat()
# Assume UTC if timezone is missing
if value.tzinfo is None:
out = out + "Z"
return out
def deserialize(self, value):
return (
value if isinstance(value, datetime) else parse(cast(str, value))
)
# This class pulls in both JsonSchemaMixin from Hologram and
# DataClassDictMixin from our fork of Mashumaro. The 'to_dict'
# and 'from_dict' methods come from Mashumaro. Building
# jsonschemas for every class and the 'validate' method
# come from Hologram.
class dbtClassMixin(DataClassDictMixin, JsonSchemaMixin):
"""Mixin which adds methods to generate a JSON schema and
convert to and from JSON encodable dicts with validation
against the schema
"""
class Config(MashBaseConfig):
code_generation_options = [
TO_DICT_ADD_OMIT_NONE_FLAG,
]
serialization_strategy = {
datetime: DateTimeSerialization(),
}
_hyphenated: ClassVar[bool] = False
ADDITIONAL_PROPERTIES: ClassVar[bool] = False
# This is called by the mashumaro to_dict in order to handle
# nested classes.
# Munges the dict that's returned.
def __post_serialize__(self, dct):
if self._hyphenated:
new_dict = {}
for key in dct:
if '_' in key:
new_key = key.replace('_', '-')
new_dict[new_key] = dct[key]
else:
new_dict[key] = dct[key]
dct = new_dict
return dct
# This is called by the mashumaro _from_dict method, before
# performing the conversion to a dict
@classmethod
def __pre_deserialize__(cls, data):
# `data` might not be a dict, e.g. for `query_comment`, which accepts
# a dict or a string; only snake-case for dict values.
if cls._hyphenated and isinstance(data, dict):
new_dict = {}
for key in data:
if '-' in key:
new_key = key.replace('-', '_')
new_dict[new_key] = data[key]
else:
new_dict[key] = data[key]
data = new_dict
return data
# This is used in the hologram._encode_field method, which calls
# a 'to_dict' method which does not have the same parameters in
# hologram and in mashumaro.
def _local_to_dict(self, **kwargs):
args = {}
if 'omit_none' in kwargs:
args['omit_none'] = kwargs['omit_none']
return self.to_dict(**args)
class ValidatedStringMixin(str, SerializableType):
ValidationRegex = ''
@classmethod
def _deserialize(cls, value: str) -> 'ValidatedStringMixin':
cls.validate(value)
return ValidatedStringMixin(value)
def _serialize(self) -> str:
return str(self)
@classmethod
def validate(cls, value):
res = re.match(cls.ValidationRegex, value)
if res is None:
raise ValidationError(f"Invalid value: {value}") # TODO
# These classes must be in this order or it doesn't work
class StrEnum(str, SerializableType, Enum):
def __str__(self):
return self.value
# https://docs.python.org/3.6/library/enum.html#using-automatic-values
def _generate_next_value_(name, *_):
return name
def _serialize(self) -> str:
return self.value
@classmethod
def _deserialize(cls, value: str):
return cls(value)
class HyphenatedDbtClassMixin(dbtClassMixin):
# used by from_dict/to_dict
_hyphenated: ClassVar[bool] = True
# used by jsonschema validation, _get_fields
@classmethod
def field_mapping(cls):
result = {}
for field in fields(cls):
skip = field.metadata.get("preserve_underscore")
if skip:
continue
if "_" in field.name:
result[field.name] = field.name.replace("_", "-")
return result
class ExtensibleDbtClassMixin(dbtClassMixin):
ADDITIONAL_PROPERTIES = True
# This is used by Hologram in jsonschema validation
def register_pattern(base_type: Type, pattern: str) -> None:
"""base_type should be a typing.NewType that should always have the given
regex pattern. That means that its underlying type ('__supertype__') had
better be a str!
"""
class PatternEncoder(FieldEncoder):
@property
def json_schema(self):
return {"type": "string", "pattern": pattern}
dbtClassMixin.register_field_encoders({base_type: PatternEncoder()})

View File

@@ -1,9 +1,9 @@
from typing import Optional, Set, List, Dict, ClassVar
import dbt.links
import dbt.exceptions
import dbt.flags
from dbt.ui import printer
from dbt import ui
import dbt.tracking
class DBTDeprecation:
@@ -18,6 +18,12 @@ class DBTDeprecation:
'name not implemented for {}'.format(self)
)
def track_deprecation_warn(self) -> None:
if dbt.tracking.active_user is not None:
dbt.tracking.track_deprecation_warn({
"deprecation_name": self.name
})
@property
def description(self) -> str:
if self._description is not None:
@@ -29,26 +35,25 @@ class DBTDeprecation:
def show(self, *args, **kwargs) -> None:
if self.name not in active_deprecations:
desc = self.description.format(**kwargs)
msg = printer.line_wrap_message(
msg = ui.line_wrap_message(
desc, prefix='* Deprecation Warning: '
)
dbt.exceptions.warn_or_error(msg)
self.track_deprecation_warn()
active_deprecations.add(self.name)
class GenerateSchemaNameSingleArgDeprecated(DBTDeprecation):
_name = 'generate-schema-name-single-arg'
class DispatchPackagesDeprecation(DBTDeprecation):
_name = 'dispatch-packages'
_description = '''\
As of dbt v0.14.0, the `generate_schema_name` macro accepts a second "node"
argument. The one-argument form of `generate_schema_name` is deprecated,
and will become unsupported in a future release.
The "packages" argument of adapter.dispatch() has been deprecated.
Use the "macro_namespace" argument instead.
Raised during dispatch for: {macro_name}
For more information, see:
https://docs.getdbt.com/v0.14/docs/upgrading-to-014
https://docs.getdbt.com/reference/dbt-jinja-functions/dispatch
'''
@@ -108,23 +113,29 @@ class ModelsKeyNonModelDeprecation(DBTDeprecation):
'''
class BigQueryPartitionByStringDeprecation(DBTDeprecation):
_name = 'bq-partition-by-string'
_description = '''
As of dbt v0.16.0, the `partition_by` config in BigQuery accepts a
dictionary containing `field` and `data_type`.
class ExecuteMacrosReleaseDeprecation(DBTDeprecation):
_name = 'execute-macro-release'
_description = '''\
The "release" argument to execute_macro is now ignored, and will be removed
in a future relase of dbt. At that time, providing a `release` argument
will result in an error.
'''
- Provided partition_by: {raw_partition_by}
class AdapterMacroDeprecation(DBTDeprecation):
_name = 'adapter-macro'
_description = '''\
The "adapter_macro" macro has been deprecated. Instead, use the
`adapter.dispatch` method to find a macro and call the result.
adapter_macro was called for: {macro_name}
'''
- dbt inferred: {inferred_partition_by}
For more information, see:
https://docs.getdbt.com/docs/upgrading-to-0-16-0
class PackageRedirectDeprecation(DBTDeprecation):
_name = 'package-redirect'
_description = '''\
The `{old_name}` package is deprecated in favor of `{new_name}`. Please update
your `packages.yml` configuration to use `{new_name}` instead.
'''
@@ -166,12 +177,14 @@ def warn(name, *args, **kwargs):
active_deprecations: Set[str] = set()
deprecations_list: List[DBTDeprecation] = [
GenerateSchemaNameSingleArgDeprecated(),
DispatchPackagesDeprecation(),
MaterializationReturnDeprecation(),
NotADictionaryDeprecation(),
ColumnQuotingDeprecation(),
ModelsKeyNonModelDeprecation(),
BigQueryPartitionByStringDeprecation(),
ExecuteMacrosReleaseDeprecation(),
AdapterMacroDeprecation(),
PackageRedirectDeprecation()
]
deprecations: Dict[str, DBTDeprecation] = {

View File

@@ -93,6 +93,9 @@ class PinnedPackage(BasePackage):
dest_dirname = self.get_project_name(project, renderer)
return os.path.join(project.modules_path, dest_dirname)
def get_subdirectory(self):
return None
SomePinned = TypeVar('SomePinned', bound=PinnedPackage)
SomeUnpinned = TypeVar('SomeUnpinned', bound='UnpinnedPackage')

View File

@@ -1,6 +1,6 @@
import os
import hashlib
from typing import List
from typing import List, Optional
from dbt.clients import git, system
from dbt.config import Project
@@ -13,7 +13,7 @@ from dbt.exceptions import (
ExecutableError, warn_or_error, raise_dependency_error
)
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.ui import printer
from dbt import ui
PIN_PACKAGE_URL = 'https://docs.getdbt.com/docs/package-management#section-specifying-package-versions' # noqa
@@ -37,18 +37,37 @@ class GitPackageMixin:
class GitPinnedPackage(GitPackageMixin, PinnedPackage):
def __init__(
self, git: str, revision: str, warn_unpinned: bool = True
self,
git: str,
revision: str,
warn_unpinned: bool = True,
subdirectory: Optional[str] = None,
) -> None:
super().__init__(git)
self.revision = revision
self.warn_unpinned = warn_unpinned
self.subdirectory = subdirectory
self._checkout_name = md5sum(self.git)
def get_version(self):
return self.revision
def get_subdirectory(self):
return self.subdirectory
def nice_version_name(self):
return 'revision {}'.format(self.revision)
if self.revision == 'HEAD':
return 'HEAD (default revision)'
else:
return 'revision {}'.format(self.revision)
def unpinned_msg(self):
if self.revision == 'HEAD':
return 'not pinned, using HEAD (default branch)'
elif self.revision in ('main', 'master'):
return f'pinned to the "{self.revision}" branch'
else:
return None
def _checkout(self):
"""Performs a shallow clone of the repository into the downloads
@@ -57,8 +76,8 @@ class GitPinnedPackage(GitPackageMixin, PinnedPackage):
the path to the checked out directory."""
try:
dir_ = git.clone_and_checkout(
self.git, get_downloads_path(), branch=self.revision,
dirname=self._checkout_name
self.git, get_downloads_path(), revision=self.revision,
dirname=self._checkout_name, subdirectory=self.subdirectory
)
except ExecutableError as exc:
if exc.cmd and exc.cmd[0] == 'git':
@@ -72,12 +91,13 @@ class GitPinnedPackage(GitPackageMixin, PinnedPackage):
def _fetch_metadata(self, project, renderer) -> ProjectPackageMetadata:
path = self._checkout()
if self.revision == 'master' and self.warn_unpinned:
if self.unpinned_msg() and self.warn_unpinned:
warn_or_error(
'The git package "{}" is not pinned.\n\tThis can introduce '
'The git package "{}" \n\tis {}.\n\tThis can introduce '
'breaking changes into your project without warning!\n\nSee {}'
.format(self.git, PIN_PACKAGE_URL),
log_fmt=printer.yellow('WARNING: {}')
.format(self.git, self.unpinned_msg(), PIN_PACKAGE_URL),
log_fmt=ui.yellow('WARNING: {}')
)
loaded = Project.from_project_root(path, renderer)
return ProjectPackageMetadata.from_project(loaded)
@@ -95,22 +115,27 @@ class GitPinnedPackage(GitPackageMixin, PinnedPackage):
class GitUnpinnedPackage(GitPackageMixin, UnpinnedPackage[GitPinnedPackage]):
def __init__(
self, git: str, revisions: List[str], warn_unpinned: bool = True
self,
git: str,
revisions: List[str],
warn_unpinned: bool = True,
subdirectory: Optional[str] = None,
) -> None:
super().__init__(git)
self.revisions = revisions
self.warn_unpinned = warn_unpinned
self.subdirectory = subdirectory
@classmethod
def from_contract(
cls, contract: GitPackage
) -> 'GitUnpinnedPackage':
revisions = [contract.revision] if contract.revision else []
revisions = contract.get_revisions()
# we want to map None -> True
warn_unpinned = contract.warn_unpinned is not False
return cls(git=contract.git, revisions=revisions,
warn_unpinned=warn_unpinned)
warn_unpinned=warn_unpinned, subdirectory=contract.subdirectory)
def all_names(self) -> List[str]:
if self.git.endswith('.git'):
@@ -128,12 +153,13 @@ class GitUnpinnedPackage(GitPackageMixin, UnpinnedPackage[GitPinnedPackage]):
git=self.git,
revisions=self.revisions + other.revisions,
warn_unpinned=warn_unpinned,
subdirectory=self.subdirectory,
)
def resolved(self) -> GitPinnedPackage:
requested = set(self.revisions)
if len(requested) == 0:
requested = {'master'}
requested = {'HEAD'}
elif len(requested) > 1:
raise_dependency_error(
'git dependencies should contain exactly one version. '
@@ -141,5 +167,5 @@ class GitUnpinnedPackage(GitPackageMixin, UnpinnedPackage[GitPinnedPackage]):
return GitPinnedPackage(
git=self.git, revision=requested.pop(),
warn_unpinned=self.warn_unpinned
warn_unpinned=self.warn_unpinned, subdirectory=self.subdirectory
)

View File

@@ -30,9 +30,13 @@ class RegistryPackageMixin:
class RegistryPinnedPackage(RegistryPackageMixin, PinnedPackage):
def __init__(self, package: str, version: str) -> None:
def __init__(self,
package: str,
version: str,
version_latest: str) -> None:
super().__init__(package)
self.version = version
self.version_latest = version_latest
@property
def name(self):
@@ -44,6 +48,9 @@ class RegistryPinnedPackage(RegistryPackageMixin, PinnedPackage):
def get_version(self):
return self.version
def get_version_latest(self):
return self.version_latest
def nice_version_name(self):
return 'version {}'.format(self.version)
@@ -61,7 +68,7 @@ class RegistryPinnedPackage(RegistryPackageMixin, PinnedPackage):
system.make_directory(os.path.dirname(tar_path))
download_url = metadata.downloads.tarball
system.download(download_url, tar_path)
system.download_with_retries(download_url, tar_path)
deps_path = project.modules_path
package_name = self.get_project_name(project, renderer)
system.untar_package(tar_path, deps_path, package_name)
@@ -71,10 +78,14 @@ class RegistryUnpinnedPackage(
RegistryPackageMixin, UnpinnedPackage[RegistryPinnedPackage]
):
def __init__(
self, package: str, versions: List[semver.VersionSpecifier]
self,
package: str,
versions: List[semver.VersionSpecifier],
install_prerelease: bool
) -> None:
super().__init__(package)
self.versions = versions
self.install_prerelease = install_prerelease
def _check_in_index(self):
index = registry.index_cached()
@@ -85,21 +96,24 @@ class RegistryUnpinnedPackage(
def from_contract(
cls, contract: RegistryPackage
) -> 'RegistryUnpinnedPackage':
raw_version = contract.version
if isinstance(raw_version, str):
raw_version = [raw_version]
raw_version = contract.get_versions()
versions = [
semver.VersionSpecifier.from_version_string(v)
for v in raw_version
]
return cls(package=contract.package, versions=versions)
return cls(
package=contract.package,
versions=versions,
install_prerelease=contract.install_prerelease
)
def incorporate(
self, other: 'RegistryUnpinnedPackage'
) -> 'RegistryUnpinnedPackage':
return RegistryUnpinnedPackage(
package=self.package,
install_prerelease=self.install_prerelease,
versions=self.versions + other.versions,
)
@@ -113,12 +127,18 @@ class RegistryUnpinnedPackage(
raise DependencyException(new_msg) from e
available = registry.get_available_versions(self.package)
installable = semver.filter_installable(
available,
self.install_prerelease
)
available_latest = installable[-1]
# for now, pick a version and then recurse. later on,
# we'll probably want to traverse multiple options
# so we can match packages. not going to make a difference
# right now.
target = semver.resolve_to_specific_version(range_, available)
target = semver.resolve_to_specific_version(range_, installable)
if not target:
package_version_not_found(self.package, range_, available)
return RegistryPinnedPackage(package=self.package, version=target)
package_version_not_found(self.package, range_, installable)
return RegistryPinnedPackage(package=self.package, version=target,
version_latest=available_latest)

View File

@@ -4,7 +4,8 @@ from typing import Dict, List, NoReturn, Union, Type, Iterator, Set
from dbt.exceptions import raise_dependency_error, InternalException
from dbt.context.target import generate_target_context
from dbt.config import Project, ConfigRenderer, RuntimeConfig
from dbt.config import Project, RuntimeConfig
from dbt.config.renderer import DbtProjectYamlRenderer
from dbt.deps.base import BasePackage, PinnedPackage, UnpinnedPackage
from dbt.deps.local import LocalUnpinnedPackage
from dbt.deps.git import GitUnpinnedPackage
@@ -97,7 +98,9 @@ class PackageListing:
def _check_for_duplicate_project_names(
final_deps: List[PinnedPackage], config: Project, renderer: ConfigRenderer
final_deps: List[PinnedPackage],
config: Project,
renderer: DbtProjectYamlRenderer,
):
seen: Set[str] = set()
for package in final_deps:
@@ -123,7 +126,8 @@ def resolve_packages(
pending = PackageListing.from_contracts(packages)
final = PackageListing()
renderer = ConfigRenderer(generate_target_context(config, config.cli_vars))
ctx = generate_target_context(config, config.cli_vars)
renderer = DbtProjectYamlRenderer(ctx)
while pending:
next_pending = PackageListing()

View File

@@ -1,19 +1,20 @@
import builtins
import functools
from typing import NoReturn, Optional
from typing import NoReturn, Optional, Mapping, Any
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.node_types import NodeType
import dbt.flags
from dbt import flags
from dbt.ui import line_wrap_message
import hologram
import dbt.dataclass_schema
def validator_error_message(exc):
"""Given a hologram.ValidationError (which is basically a
"""Given a dbt.dataclass_schema.ValidationError (which is basically a
jsonschema.ValidationError), return the relevant parts as a string
"""
if not isinstance(exc, hologram.ValidationError):
if not isinstance(exc, dbt.dataclass_schema.ValidationError):
return str(exc)
path = "[%s]" % "][".join(map(repr, exc.relative_path))
return 'at path {}: {}'.format(path, exc.message)
@@ -53,6 +54,12 @@ class RuntimeException(RuntimeError, Exception):
self.node = node
self.msg = msg
def add_node(self, node=None):
if node is not None and node is not self.node:
if self.node is not None:
self.stack.append(self.node)
self.node = node
@property
def type(self):
return 'Runtime'
@@ -125,7 +132,7 @@ class RuntimeException(RuntimeError, Exception):
result.update({
'raw_sql': self.node.raw_sql,
# the node isn't always compiled, but if it is, include that!
'compiled_sql': getattr(self.node, 'injected_sql', None),
'compiled_sql': getattr(self.node, 'compiled_sql', None),
})
return result
@@ -250,6 +257,47 @@ class JSONValidationException(ValidationException):
return (JSONValidationException, (self.typename, self.errors))
class IncompatibleSchemaException(RuntimeException):
def __init__(self, expected: str, found: Optional[str]):
self.expected = expected
self.found = found
self.filename = 'input file'
super().__init__(self.get_message())
def add_filename(self, filename: str):
self.filename = filename
self.msg = self.get_message()
def get_message(self) -> str:
found_str = 'nothing'
if self.found is not None:
found_str = f'"{self.found}"'
msg = (
f'Expected a schema version of "{self.expected}" in '
f'{self.filename}, but found {found_str}. Are you running with a '
f'different version of dbt?'
)
return msg
CODE = 10014
MESSAGE = "Incompatible Schema"
class JinjaRenderingException(CompilationException):
pass
class UndefinedMacroException(CompilationException):
def __str__(self, prefix='! ') -> str:
msg = super().__str__(prefix)
return f'{msg}. This can happen when calling a macro that does ' \
'not exist. Check for typos and/or install package dependencies ' \
'with "dbt deps".'
class UnknownAsyncIDException(Exception):
CODE = 10012
MESSAGE = 'RPC server got an unknown async ID'
@@ -275,16 +323,43 @@ class DbtConfigError(RuntimeException):
CODE = 10007
MESSAGE = "DBT Configuration Error"
def __init__(self, message, project=None, result_type='invalid_project'):
def __init__(
self, message, project=None, result_type='invalid_project', path=None
):
self.project = project
super().__init__(message)
self.result_type = result_type
self.path = path
def __str__(self, prefix='! ') -> str:
msg = super().__str__(prefix)
if self.path is None:
return msg
else:
return f'{msg}\n\nError encountered in {self.path}'
class FailFastException(RuntimeException):
CODE = 10013
MESSAGE = 'FailFast Error'
def __init__(self, message, result=None, node=None):
super().__init__(msg=message, node=node)
self.result = result
@property
def type(self):
return 'FailFast'
class DbtProjectError(DbtConfigError):
pass
class DbtSelectorsError(DbtConfigError):
pass
class DbtProfileError(DbtConfigError):
pass
@@ -359,6 +434,12 @@ class InvalidConnectionException(RuntimeException):
)
class InvalidSelectorException(RuntimeException):
def __init__(self, name: str):
self.name = name
super().__init__(name)
def raise_compiler_error(msg, node=None) -> NoReturn:
raise CompilationException(msg, node)
@@ -443,8 +524,10 @@ def doc_target_not_found(
raise_compiler_error(msg, model)
def _get_target_failure_msg(model, target_model_name, target_model_package,
include_path, reason):
def _get_target_failure_msg(
model, target_name: str, target_model_package: Optional[str],
include_path: bool, reason: str, target_kind: str
) -> str:
target_package_string = ''
if target_model_package is not None:
target_package_string = "in package '{}' ".format(target_model_package)
@@ -453,52 +536,73 @@ def _get_target_failure_msg(model, target_model_name, target_model_package,
if include_path:
source_path_string = ' ({})'.format(model.original_file_path)
return "{} '{}'{} depends on a node named '{}' {}which {}".format(
return "{} '{}'{} depends on a {} named '{}' {}which {}".format(
model.resource_type.title(),
model.unique_id,
source_path_string,
target_model_name,
target_kind,
target_name,
target_package_string,
reason
)
def get_target_disabled_msg(model, target_model_name, target_model_package):
return _get_target_failure_msg(model, target_model_name,
target_model_package, include_path=True,
reason='is disabled')
def get_target_not_found_or_disabled_msg(
model, target_model_name: str, target_model_package: Optional[str],
disabled: Optional[bool] = None,
) -> str:
if disabled is None:
reason = 'was not found or is disabled'
elif disabled is True:
reason = 'is disabled'
else:
reason = 'was not found'
return _get_target_failure_msg(
model, target_model_name, target_model_package, include_path=True,
reason=reason, target_kind='node'
)
def get_target_not_found_msg(model, target_model_name, target_model_package):
return _get_target_failure_msg(model, target_model_name,
target_model_package, include_path=True,
reason='was not found')
def get_target_not_found_or_disabled_msg(model, target_model_name,
target_model_package):
return _get_target_failure_msg(model, target_model_name,
target_model_package, include_path=False,
reason='was not found or is disabled')
def ref_target_not_found(model, target_model_name, target_model_package):
msg = get_target_not_found_or_disabled_msg(model, target_model_name,
target_model_package)
def ref_target_not_found(
model,
target_model_name: str,
target_model_package: Optional[str],
disabled: Optional[bool] = None,
) -> NoReturn:
msg = get_target_not_found_or_disabled_msg(
model, target_model_name, target_model_package, disabled
)
raise_compiler_error(msg, model)
def source_disabled_message(model, target_name, target_table_name):
return ("{} '{}' ({}) depends on source '{}.{}' which was not found"
.format(model.resource_type.title(),
model.unique_id,
model.original_file_path,
target_name,
target_table_name))
def get_source_not_found_or_disabled_msg(
model,
target_name: str,
target_table_name: str,
disabled: Optional[bool] = None,
) -> str:
full_name = f'{target_name}.{target_table_name}'
if disabled is None:
reason = 'was not found or is disabled'
elif disabled is True:
reason = 'is disabled'
else:
reason = 'was not found'
return _get_target_failure_msg(
model, full_name, None, include_path=True,
reason=reason, target_kind='source'
)
def source_target_not_found(model, target_name, target_table_name) -> NoReturn:
msg = source_disabled_message(model, target_name, target_table_name)
def source_target_not_found(
model,
target_name: str,
target_table_name: str,
disabled: Optional[bool] = None
) -> NoReturn:
msg = get_source_not_found_or_disabled_msg(
model, target_name, target_table_name, disabled
)
raise_compiler_error(msg, model)
@@ -606,11 +710,11 @@ def system_error(operation_name):
raise_compiler_error(
"dbt encountered an error when attempting to {}. "
"If this error persists, please create an issue at: \n\n"
"https://github.com/fishtown-analytics/dbt"
"https://github.com/dbt-labs/dbt"
.format(operation_name))
class RegistryException(Exception):
class ConnectionException(Exception):
pass
@@ -644,10 +748,10 @@ def approximate_relation_match(target, relation):
def raise_duplicate_macro_name(node_1, node_2, namespace) -> NoReturn:
duped_name = node_1.namespace
duped_name = node_1.name
if node_1.package_name != node_2.package_name:
extra = (
' ({} and {} are both in the {} namespace)'
' ("{}" and "{}" are both in the "{}" namespace)'
.format(node_1.package_name, node_2.package_name, namespace)
)
else:
@@ -672,11 +776,14 @@ def raise_duplicate_resource_name(node_1, node_2):
if node_1.resource_type in NodeType.refable():
get_func = 'ref("{}")'.format(duped_name)
elif node_1.resource_type == NodeType.Source:
get_func = 'source("{}", "{}")'.format(node_1.source_name, duped_name)
duped_name = node_1.get_full_source_name()
get_func = node_1.get_source_representation()
elif node_1.resource_type == NodeType.Documentation:
get_func = 'doc("{}")'.format(duped_name)
elif node_1.resource_type == NodeType.Test and 'schema' in node_1.tags:
return
else:
get_func = '"{}"'.format(duped_name)
raise_compiler_error(
'dbt found two resources with the name "{}". Since these resources '
@@ -689,14 +796,15 @@ def raise_duplicate_resource_name(node_1, node_2):
node_2.unique_id, node_2.original_file_path))
def raise_ambiguous_alias(node_1, node_2):
duped_name = "{}.{}".format(node_1.schema, node_1.alias)
def raise_ambiguous_alias(node_1, node_2, duped_name=None):
if duped_name is None:
duped_name = f"{node_1.database}.{node_1.schema}.{node_1.alias}"
raise_compiler_error(
'dbt found two resources with the database representation "{}".\ndbt '
'cannot create two resources with identical database representations. '
'To fix this,\nchange the "schema" or "alias" configuration of one of '
'these resources:\n- {} ({})\n- {} ({})'.format(
'To fix this,\nchange the configuration of one of these resources:'
'\n- {} ({})\n- {} ({})'.format(
duped_name,
node_1.unique_id, node_1.original_file_path,
node_2.unique_id, node_2.original_file_path))
@@ -733,26 +841,62 @@ def raise_patch_targets_not_found(patches):
)
def raise_duplicate_patch_name(patch_1, patch_2):
def _fix_dupe_msg(path_1: str, path_2: str, name: str, type_name: str) -> str:
if path_1 == path_2:
return (
f'remove one of the {type_name} entries for {name} in this file:\n'
f' - {path_1!s}\n'
)
else:
return (
f'remove the {type_name} entry for {name} in one of these files:\n'
f' - {path_1!s}\n{path_2!s}'
)
def raise_duplicate_patch_name(patch_1, existing_patch_path):
name = patch_1.name
fix = _fix_dupe_msg(
patch_1.original_file_path,
existing_patch_path,
name,
'resource',
)
raise_compiler_error(
f'dbt found two schema.yml entries for the same resource named '
f'{name}. Resources and their associated columns may only be '
f'described a single time. To fix this, remove the resource entry '
f'for {name} in one of these files:\n - '
f'{patch_1.original_file_path}\n - {patch_2.original_file_path}'
f'described a single time. To fix this, {fix}'
)
def raise_duplicate_macro_patch_name(patch_1, patch_2):
def raise_duplicate_macro_patch_name(patch_1, existing_patch_path):
package_name = patch_1.package_name
name = patch_1.name
fix = _fix_dupe_msg(
patch_1.original_file_path,
existing_patch_path,
name,
'macros'
)
raise_compiler_error(
f'dbt found two schema.yml entries for the same macro in package '
f'{package_name} named {name}. Macros may only be described a single '
f'time. To fix this, remove the macros entry for {name} in one '
f'of these files:'
f'\n - {patch_1.original_file_path}\n - {patch_2.original_file_path}'
f'time. To fix this, {fix}'
)
def raise_duplicate_source_patch_name(patch_1, patch_2):
name = f'{patch_1.overrides}.{patch_1.name}'
fix = _fix_dupe_msg(
patch_1.path,
patch_2.path,
name,
'sources',
)
raise_compiler_error(
f'dbt found two schema.yml entries for the same source named '
f'{patch_1.name} in package {patch_1.overrides}. Sources may only be '
f'overridden a single time. To fix this, {fix}'
)
@@ -772,14 +916,46 @@ def raise_unrecognized_credentials_type(typename, supported_types):
)
def raise_invalid_patch(
node, patch_section: str, patch_path: str,
) -> NoReturn:
msg = line_wrap_message(
f'''\
'{node.name}' is a {node.resource_type} node, but it is
specified in the {patch_section} section of
{patch_path}.
To fix this error, place the `{node.name}`
specification under the {node.resource_type.pluralize()} key instead.
'''
)
raise_compiler_error(msg, node)
def raise_not_implemented(msg):
raise NotImplementedException(
"ERROR: {}"
.format(msg))
def raise_duplicate_alias(
kwargs: Mapping[str, Any], aliases: Mapping[str, str], canonical_key: str
) -> NoReturn:
# dupe found: go through the dict so we can have a nice-ish error
key_names = ', '.join(
"{}".format(k) for k in kwargs if
aliases.get(k) == canonical_key
)
raise AliasException(
f'Got duplicate keys: ({key_names}) all map to "{canonical_key}"'
)
def warn_or_error(msg, node=None, log_fmt=None):
if dbt.flags.WARN_ERROR:
if flags.WARN_ERROR:
raise_compiler_error(msg, node)
else:
if log_fmt is not None:
@@ -788,7 +964,7 @@ def warn_or_error(msg, node=None, log_fmt=None):
def warn_or_raise(exc, log_fmt=None):
if dbt.flags.WARN_ERROR:
if flags.WARN_ERROR:
raise exc
else:
msg = str(exc)
@@ -800,7 +976,8 @@ def warn_or_raise(exc, log_fmt=None):
def warn(msg, node=None):
# there's no reason to expose log_fmt to macros - it's only useful for
# handling colors
return warn_or_error(msg, node=node)
warn_or_error(msg, node=node)
return ""
# Update this when a new function should be added to the
@@ -836,8 +1013,7 @@ def wrapper(model):
try:
return func(*args, **kwargs)
except RuntimeException as exc:
if exc.node is None:
exc.node = model
exc.add_node(model)
raise exc
return inner
return wrap

View File

@@ -1,6 +1,11 @@
import os
import multiprocessing
if os.name != 'nt':
# https://bugs.python.org/issue41567
import multiprocessing.popen_spawn_posix # type: ignore
from pathlib import Path
from typing import Optional
# initially all flags are set to None, the on-load call of reset() will set
# them for their first time.
STRICT_MODE = None
@@ -8,8 +13,11 @@ FULL_REFRESH = None
USE_CACHE = None
WARN_ERROR = None
TEST_NEW_PARSER = None
USE_EXPERIMENTAL_PARSER = None
WRITE_JSON = None
PARTIAL_PARSE = None
USE_COLORS = None
STORE_FAILURES = None
def env_set_truthy(key: str) -> Optional[str]:
@@ -22,19 +30,24 @@ def env_set_truthy(key: str) -> Optional[str]:
return value
def env_set_path(key: str) -> Optional[Path]:
value = os.getenv(key)
if value is None:
return value
else:
return Path(value)
SINGLE_THREADED_WEBSERVER = env_set_truthy('DBT_SINGLE_THREADED_WEBSERVER')
SINGLE_THREADED_HANDLER = env_set_truthy('DBT_SINGLE_THREADED_HANDLER')
MACRO_DEBUGGING = env_set_truthy('DBT_MACRO_DEBUGGING')
DEFER_MODE = env_set_truthy('DBT_DEFER_TO_STATE')
ARTIFACT_STATE_PATH = env_set_path('DBT_ARTIFACT_STATE_PATH')
def _get_context():
if os.name == 'posix' and os.uname().sysname.lower() != 'darwin':
# on linux fork is available and it's fast
return multiprocessing.get_context('fork')
else:
# on windows, spawn is the only choice.
# On osx, fork is buggy: https://bugs.python.org/issue33725
return multiprocessing.get_context('spawn')
# TODO: change this back to use fork() on linux when we have made that safe
return multiprocessing.get_context('spawn')
MP_CONTEXT = _get_context()
@@ -42,21 +55,26 @@ MP_CONTEXT = _get_context()
def reset():
global STRICT_MODE, FULL_REFRESH, USE_CACHE, WARN_ERROR, TEST_NEW_PARSER, \
WRITE_JSON, PARTIAL_PARSE, MP_CONTEXT
USE_EXPERIMENTAL_PARSER, WRITE_JSON, PARTIAL_PARSE, MP_CONTEXT, USE_COLORS, \
STORE_FAILURES
STRICT_MODE = False
FULL_REFRESH = False
USE_CACHE = True
WARN_ERROR = False
TEST_NEW_PARSER = False
USE_EXPERIMENTAL_PARSER = False
WRITE_JSON = True
PARTIAL_PARSE = False
MP_CONTEXT = _get_context()
USE_COLORS = True
STORE_FAILURES = False
def set_from_args(args):
global STRICT_MODE, FULL_REFRESH, USE_CACHE, WARN_ERROR, TEST_NEW_PARSER, \
WRITE_JSON, PARTIAL_PARSE, MP_CONTEXT
USE_EXPERIMENTAL_PARSER, WRITE_JSON, PARTIAL_PARSE, MP_CONTEXT, USE_COLORS, \
STORE_FAILURES
USE_CACHE = getattr(args, 'use_cache', USE_CACHE)
@@ -68,10 +86,20 @@ def set_from_args(args):
)
TEST_NEW_PARSER = getattr(args, 'test_new_parser', TEST_NEW_PARSER)
USE_EXPERIMENTAL_PARSER = getattr(args, 'use_experimental_parser', USE_EXPERIMENTAL_PARSER)
WRITE_JSON = getattr(args, 'write_json', WRITE_JSON)
PARTIAL_PARSE = getattr(args, 'partial_parse', None)
MP_CONTEXT = _get_context()
# The use_colors attribute will always have a value because it is assigned
# None by default from the add_mutually_exclusive_group function
use_colors_override = getattr(args, 'use_colors')
if use_colors_override is not None:
USE_COLORS = use_colors_override
STORE_FAILURES = getattr(args, 'store_failures', STORE_FAILURES)
# initialize everything to the defaults on module load
reset()

View File

@@ -0,0 +1,18 @@
from .selector_spec import ( # noqa: F401
SelectionUnion,
SelectionSpec,
SelectionIntersection,
SelectionDifference,
SelectionCriteria,
)
from .selector import ( # noqa: F401
ResourceTypeSelector,
NodeSelector,
)
from .cli import ( # noqa: F401
parse_difference,
parse_test_selectors,
parse_from_selectors_definition,
)
from .queue import GraphQueue # noqa: F401
from .graph import Graph, UniqueId # noqa: F401

280
core/dbt/graph/cli.py Normal file
View File

@@ -0,0 +1,280 @@
# special support for CLI argument parsing.
import itertools
from dbt.clients.yaml_helper import yaml, Loader, Dumper # noqa: F401
from typing import (
Dict, List, Optional, Tuple, Any, Union
)
from dbt.contracts.selection import SelectorDefinition, SelectorFile
from dbt.exceptions import InternalException, ValidationException
from .selector_spec import (
SelectionUnion,
SelectionSpec,
SelectionIntersection,
SelectionDifference,
SelectionCriteria,
)
INTERSECTION_DELIMITER = ','
DEFAULT_INCLUDES: List[str] = ['fqn:*', 'source:*', 'exposure:*']
DEFAULT_EXCLUDES: List[str] = []
DATA_TEST_SELECTOR: str = 'test_type:data'
SCHEMA_TEST_SELECTOR: str = 'test_type:schema'
def parse_union(
components: List[str], expect_exists: bool, greedy: bool = False
) -> SelectionUnion:
# turn ['a b', 'c'] -> ['a', 'b', 'c']
raw_specs = itertools.chain.from_iterable(
r.split(' ') for r in components
)
union_components: List[SelectionSpec] = []
# ['a', 'b', 'c,d'] -> union('a', 'b', intersection('c', 'd'))
for raw_spec in raw_specs:
intersection_components: List[SelectionSpec] = [
SelectionCriteria.from_single_spec(part, greedy=greedy)
for part in raw_spec.split(INTERSECTION_DELIMITER)
]
union_components.append(SelectionIntersection(
components=intersection_components,
expect_exists=expect_exists,
raw=raw_spec,
))
return SelectionUnion(
components=union_components,
expect_exists=False,
raw=components,
)
def parse_union_from_default(
raw: Optional[List[str]], default: List[str], greedy: bool = False
) -> SelectionUnion:
components: List[str]
expect_exists: bool
if raw is None:
return parse_union(components=default, expect_exists=False, greedy=greedy)
else:
return parse_union(components=raw, expect_exists=True, greedy=greedy)
def parse_difference(
include: Optional[List[str]], exclude: Optional[List[str]]
) -> SelectionDifference:
included = parse_union_from_default(include, DEFAULT_INCLUDES)
excluded = parse_union_from_default(exclude, DEFAULT_EXCLUDES, greedy=True)
return SelectionDifference(components=[included, excluded])
def parse_test_selectors(
data: bool, schema: bool, base: SelectionSpec
) -> SelectionSpec:
union_components = []
if data:
union_components.append(
SelectionCriteria.from_single_spec(DATA_TEST_SELECTOR)
)
if schema:
union_components.append(
SelectionCriteria.from_single_spec(SCHEMA_TEST_SELECTOR)
)
intersect_with: SelectionSpec
if not union_components:
return base
elif len(union_components) == 1:
intersect_with = union_components[0]
else: # data and schema tests
intersect_with = SelectionUnion(
components=union_components,
expect_exists=True,
raw=[DATA_TEST_SELECTOR, SCHEMA_TEST_SELECTOR],
)
return SelectionIntersection(
components=[base, intersect_with], expect_exists=True
)
RawDefinition = Union[str, Dict[str, Any]]
def _get_list_dicts(
dct: Dict[str, Any], key: str
) -> List[RawDefinition]:
result: List[RawDefinition] = []
if key not in dct:
raise InternalException(
f'Expected to find key {key} in dict, only found {list(dct)}'
)
values = dct[key]
if not isinstance(values, list):
raise ValidationException(
f'Invalid value for key "{key}". Expected a list.'
)
for value in values:
if isinstance(value, dict):
for value_key in value:
if not isinstance(value_key, str):
raise ValidationException(
f'Expected all keys to "{key}" dict to be strings, '
f'but "{value_key}" is a "{type(value_key)}"'
)
result.append(value)
elif isinstance(value, str):
result.append(value)
else:
raise ValidationException(
f'Invalid value type {type(value)} in key "{key}", expected '
f'dict or str (value: {value}).'
)
return result
def _parse_exclusions(definition) -> Optional[SelectionSpec]:
exclusions = _get_list_dicts(definition, 'exclude')
parsed_exclusions = [
parse_from_definition(excl) for excl in exclusions
]
if len(parsed_exclusions) == 1:
return parsed_exclusions[0]
elif len(parsed_exclusions) > 1:
return SelectionUnion(
components=parsed_exclusions,
raw=exclusions
)
else:
return None
def _parse_include_exclude_subdefs(
definitions: List[RawDefinition]
) -> Tuple[List[SelectionSpec], Optional[SelectionSpec]]:
include_parts: List[SelectionSpec] = []
diff_arg: Optional[SelectionSpec] = None
for definition in definitions:
if isinstance(definition, dict) and 'exclude' in definition:
# do not allow multiple exclude: defs at the same level
if diff_arg is not None:
yaml_sel_cfg = yaml.dump(definition)
raise ValidationException(
f"You cannot provide multiple exclude arguments to the "
f"same selector set operator:\n{yaml_sel_cfg}"
)
diff_arg = _parse_exclusions(definition)
else:
include_parts.append(parse_from_definition(definition))
return (include_parts, diff_arg)
def parse_union_definition(definition: Dict[str, Any]) -> SelectionSpec:
union_def_parts = _get_list_dicts(definition, 'union')
include, exclude = _parse_include_exclude_subdefs(union_def_parts)
union = SelectionUnion(components=include)
if exclude is None:
union.raw = definition
return union
else:
return SelectionDifference(
components=[union, exclude],
raw=definition
)
def parse_intersection_definition(
definition: Dict[str, Any]
) -> SelectionSpec:
intersection_def_parts = _get_list_dicts(definition, 'intersection')
include, exclude = _parse_include_exclude_subdefs(intersection_def_parts)
intersection = SelectionIntersection(components=include)
if exclude is None:
intersection.raw = definition
return intersection
else:
return SelectionDifference(
components=[intersection, exclude],
raw=definition
)
def parse_dict_definition(definition: Dict[str, Any]) -> SelectionSpec:
diff_arg: Optional[SelectionSpec] = None
if len(definition) == 1:
key = list(definition)[0]
value = definition[key]
if not isinstance(key, str):
raise ValidationException(
f'Expected definition key to be a "str", got one of type '
f'"{type(key)}" ({key})'
)
dct = {
'method': key,
'value': value,
}
elif 'method' in definition and 'value' in definition:
dct = definition
if 'exclude' in definition:
diff_arg = _parse_exclusions(definition)
dct = {k: v for k, v in dct.items() if k != 'exclude'}
else:
raise ValidationException(
f'Expected either 1 key or else "method" '
f'and "value" keys, but got {list(definition)}'
)
# if key isn't a valid method name, this will raise
base = SelectionCriteria.selection_criteria_from_dict(definition, dct)
if diff_arg is None:
return base
else:
return SelectionDifference(components=[base, diff_arg])
def parse_from_definition(
definition: RawDefinition, rootlevel=False
) -> SelectionSpec:
if (isinstance(definition, dict) and
('union' in definition or 'intersection' in definition) and
rootlevel and len(definition) > 1):
keys = ",".join(definition.keys())
raise ValidationException(
f"Only a single 'union' or 'intersection' key is allowed "
f"in a root level selector definition; found {keys}."
)
if isinstance(definition, str):
return SelectionCriteria.from_single_spec(definition)
elif 'union' in definition:
return parse_union_definition(definition)
elif 'intersection' in definition:
return parse_intersection_definition(definition)
elif isinstance(definition, dict):
return parse_dict_definition(definition)
else:
raise ValidationException(
f'Expected to find union, intersection, str or dict, instead '
f'found {type(definition)}: {definition}'
)
def parse_from_selectors_definition(
source: SelectorFile
) -> Dict[str, SelectionSpec]:
result: Dict[str, SelectionSpec] = {}
selector: SelectorDefinition
for selector in source.selectors:
result[selector.name] = parse_from_definition(selector.definition,
rootlevel=True)
return result

105
core/dbt/graph/graph.py Normal file
View File

@@ -0,0 +1,105 @@
from typing import (
Set, Iterable, Iterator, Optional, NewType
)
import networkx as nx # type: ignore
from dbt.exceptions import InternalException
UniqueId = NewType('UniqueId', str)
class Graph:
"""A wrapper around the networkx graph that understands SelectionCriteria
and how they interact with the graph.
"""
def __init__(self, graph):
self.graph = graph
def nodes(self) -> Set[UniqueId]:
return set(self.graph.nodes())
def edges(self):
return self.graph.edges()
def __iter__(self) -> Iterator[UniqueId]:
return iter(self.graph.nodes())
def ancestors(
self, node: UniqueId, max_depth: Optional[int] = None
) -> Set[UniqueId]:
"""Returns all nodes having a path to `node` in `graph`"""
if not self.graph.has_node(node):
raise InternalException(f'Node {node} not found in the graph!')
with nx.utils.reversed(self.graph):
anc = nx.single_source_shortest_path_length(G=self.graph,
source=node,
cutoff=max_depth)\
.keys()
return anc - {node}
def descendants(
self, node: UniqueId, max_depth: Optional[int] = None
) -> Set[UniqueId]:
"""Returns all nodes reachable from `node` in `graph`"""
if not self.graph.has_node(node):
raise InternalException(f'Node {node} not found in the graph!')
des = nx.single_source_shortest_path_length(G=self.graph,
source=node,
cutoff=max_depth)\
.keys()
return des - {node}
def select_childrens_parents(
self, selected: Set[UniqueId]
) -> Set[UniqueId]:
ancestors_for = self.select_children(selected) | selected
return self.select_parents(ancestors_for) | ancestors_for
def select_children(
self, selected: Set[UniqueId], max_depth: Optional[int] = None
) -> Set[UniqueId]:
descendants: Set[UniqueId] = set()
for node in selected:
descendants.update(self.descendants(node, max_depth))
return descendants
def select_parents(
self, selected: Set[UniqueId], max_depth: Optional[int] = None
) -> Set[UniqueId]:
ancestors: Set[UniqueId] = set()
for node in selected:
ancestors.update(self.ancestors(node, max_depth))
return ancestors
def select_successors(self, selected: Set[UniqueId]) -> Set[UniqueId]:
successors: Set[UniqueId] = set()
for node in selected:
successors.update(self.graph.successors(node))
return successors
def get_subset_graph(self, selected: Iterable[UniqueId]) -> 'Graph':
"""Create and return a new graph that is a shallow copy of the graph,
but with only the nodes in include_nodes. Transitive edges across
removed nodes are preserved as explicit new edges.
"""
new_graph = nx.algorithms.transitive_closure(self.graph)
include_nodes = set(selected)
for node in self:
if node not in include_nodes:
new_graph.remove_node(node)
for node in include_nodes:
if node not in new_graph:
raise ValueError(
"Couldn't find model '{}' -- does it exist or is "
"it disabled?".format(node)
)
return Graph(new_graph)
def subgraph(self, nodes: Iterable[UniqueId]) -> 'Graph':
return Graph(self.graph.subgraph(nodes))
def get_dependent_nodes(self, node: UniqueId):
return nx.descendants(self.graph, node)

205
core/dbt/graph/queue.py Normal file
View File

@@ -0,0 +1,205 @@
import networkx as nx # type: ignore
import threading
from queue import PriorityQueue
from typing import Dict, Set, List, Generator, Optional
from .graph import UniqueId
from dbt.contracts.graph.parsed import ParsedSourceDefinition, ParsedExposure
from dbt.contracts.graph.compiled import GraphMemberNode
from dbt.contracts.graph.manifest import Manifest
from dbt.node_types import NodeType
class GraphQueue:
"""A fancy queue that is backed by the dependency graph.
Note: this will mutate input!
This queue is thread-safe for `mark_done` calls, though you must ensure
that separate threads do not call `.empty()` or `__len__()` and `.get()` at
the same time, as there is an unlocked race!
"""
def __init__(self, graph: nx.DiGraph, manifest: Manifest, selected: Set[UniqueId]):
self.graph = graph
self.manifest = manifest
self._selected = selected
# store the queue as a priority queue.
self.inner: PriorityQueue = PriorityQueue()
# things that have been popped off the queue but not finished
# and worker thread reservations
self.in_progress: Set[UniqueId] = set()
# things that are in the queue
self.queued: Set[UniqueId] = set()
# this lock controls most things
self.lock = threading.Lock()
# store the 'score' of each node as a number. Lower is higher priority.
self._scores = self._get_scores(self.graph)
# populate the initial queue
self._find_new_additions()
# awaits after task end
self.some_task_done = threading.Condition(self.lock)
def get_selected_nodes(self) -> Set[UniqueId]:
return self._selected.copy()
def _include_in_cost(self, node_id: UniqueId) -> bool:
node = self.manifest.expect(node_id)
if node.resource_type != NodeType.Model:
return False
# must be a Model - tell mypy this won't be a Source or Exposure
assert not isinstance(node, (ParsedSourceDefinition, ParsedExposure))
if node.is_ephemeral:
return False
return True
@staticmethod
def _grouped_topological_sort(
graph: nx.DiGraph,
) -> Generator[List[str], None, None]:
"""Topological sort of given graph that groups ties.
Adapted from `nx.topological_sort`, this function returns a topo sort of a graph however
instead of arbitrarily ordering ties in the sort order, ties are grouped together in
lists.
Args:
graph: The graph to be sorted.
Returns:
A generator that yields lists of nodes, one list per graph depth level.
"""
indegree_map = {v: d for v, d in graph.in_degree() if d > 0}
zero_indegree = [v for v, d in graph.in_degree() if d == 0]
while zero_indegree:
yield zero_indegree
new_zero_indegree = []
for v in zero_indegree:
for _, child in graph.edges(v):
indegree_map[child] -= 1
if not indegree_map[child]:
new_zero_indegree.append(child)
zero_indegree = new_zero_indegree
def _get_scores(self, graph: nx.DiGraph) -> Dict[str, int]:
"""Scoring nodes for processing order.
Scores are calculated by the graph depth level. Lowest score (0) should be processed first.
Args:
graph: The graph to be scored.
Returns:
A dictionary consisting of `node name`:`score` pairs.
"""
# split graph by connected subgraphs
subgraphs = (
graph.subgraph(x) for x in nx.connected_components(nx.Graph(graph))
)
# score all nodes in all subgraphs
scores = {}
for subgraph in subgraphs:
grouped_nodes = self._grouped_topological_sort(subgraph)
for level, group in enumerate(grouped_nodes):
for node in group:
scores[node] = level
return scores
def get(
self, block: bool = True, timeout: Optional[float] = None
) -> GraphMemberNode:
"""Get a node off the inner priority queue. By default, this blocks.
This takes the lock, but only for part of it.
:param block: If True, block until the inner queue has data
:param timeout: If set, block for timeout seconds waiting for data.
:return: The node as present in the manifest.
See `queue.PriorityQueue` for more information on `get()` behavior and
exceptions.
"""
_, node_id = self.inner.get(block=block, timeout=timeout)
with self.lock:
self._mark_in_progress(node_id)
return self.manifest.expect(node_id)
def __len__(self) -> int:
"""The length of the queue is the number of tasks left for the queue to
give out, regardless of where they are. Incomplete tasks are not part
of the length.
This takes the lock.
"""
with self.lock:
return len(self.graph) - len(self.in_progress)
def empty(self) -> bool:
"""The graph queue is 'empty' if it all remaining nodes in the graph
are in progress.
This takes the lock.
"""
return len(self) == 0
def _already_known(self, node: UniqueId) -> bool:
"""Decide if a node is already known (either handed out as a task, or
in the queue).
Callers must hold the lock.
:param str node: The node ID to check
:returns bool: If the node is in progress/queued.
"""
return node in self.in_progress or node in self.queued
def _find_new_additions(self) -> None:
"""Find any nodes in the graph that need to be added to the internal
queue and add them.
"""
for node, in_degree in self.graph.in_degree():
if not self._already_known(node) and in_degree == 0:
self.inner.put((self._scores[node], node))
self.queued.add(node)
def mark_done(self, node_id: UniqueId) -> None:
"""Given a node's unique ID, mark it as done.
This method takes the lock.
:param str node_id: The node ID to mark as complete.
"""
with self.lock:
self.in_progress.remove(node_id)
self.graph.remove_node(node_id)
self._find_new_additions()
self.inner.task_done()
self.some_task_done.notify_all()
def _mark_in_progress(self, node_id: UniqueId) -> None:
"""Mark the node as 'in progress'.
Callers must hold the lock.
:param str node_id: The node ID to mark as in progress.
"""
self.queued.remove(node_id)
self.in_progress.add(node_id)
def join(self) -> None:
"""Join the queue. Blocks until all tasks are marked as done.
Make sure not to call this before the queue reports that it is empty.
"""
self.inner.join()
def wait_until_something_was_done(self) -> int:
"""Block until a task is done, then return the number of unfinished
tasks.
"""
with self.lock:
self.some_task_done.wait()
return self.inner.unfinished_tasks

View File

@@ -1,418 +1,291 @@
from enum import Enum
from itertools import chain
from typing import Set, Iterable, Union, List, Container, Tuple, Optional
import networkx as nx # type: ignore
from typing import Set, List, Optional, Tuple
from .graph import Graph, UniqueId
from .queue import GraphQueue
from .selector_methods import MethodManager
from .selector_spec import SelectionCriteria, SelectionSpec
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.utils import coalesce
from dbt.node_types import NodeType
import dbt.exceptions
SELECTOR_PARENTS = '+'
SELECTOR_CHILDREN = '+'
SELECTOR_GLOB = '*'
SELECTOR_CHILDREN_AND_ANCESTORS = '@'
SELECTOR_DELIMITER = ':'
from dbt.exceptions import (
InternalException,
InvalidSelectorException,
warn_or_error,
)
from dbt.contracts.graph.compiled import GraphMemberNode
from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.state import PreviousState
class SelectionCriteria:
def __init__(self, node_spec: str):
self.raw = node_spec
self.select_children = False
self.select_parents = False
self.select_childrens_parents = False
self.selector_type = SELECTOR_FILTERS.FQN
if node_spec.startswith(SELECTOR_CHILDREN_AND_ANCESTORS):
self.select_childrens_parents = True
node_spec = node_spec[1:]
if node_spec.startswith(SELECTOR_PARENTS):
self.select_parents = True
node_spec = node_spec[1:]
if node_spec.endswith(SELECTOR_CHILDREN):
self.select_children = True
node_spec = node_spec[:-1]
if self.select_children and self.select_childrens_parents:
raise dbt.exceptions.RuntimeException(
'Invalid node spec {} - "@" prefix and "+" suffix are '
'incompatible'.format(self.raw)
)
if SELECTOR_DELIMITER in node_spec:
selector_parts = node_spec.split(SELECTOR_DELIMITER, 1)
selector_type, self.selector_value = selector_parts
self.selector_type = SELECTOR_FILTERS(selector_type)
else:
self.selector_value = node_spec
def get_package_names(nodes):
return set([node.split(".")[1] for node in nodes])
class SELECTOR_FILTERS(str, Enum):
FQN = 'fqn'
TAG = 'tag'
SOURCE = 'source'
def __str__(self):
return self._value_
def alert_non_existence(raw_spec, nodes):
if len(nodes) == 0:
warn_or_error(
f"The selection criterion '{str(raw_spec)}' does not match"
f" any nodes"
)
def split_specs(node_specs: Iterable[str]):
specs: Set[str] = set()
for spec in node_specs:
parts = spec.split(" ")
specs.update(parts)
return specs
def get_package_names(graph):
return set([node.split(".")[1] for node in graph.nodes()])
def is_selected_node(real_node, node_selector):
for i, selector_part in enumerate(node_selector):
is_last = (i == len(node_selector) - 1)
# if we hit a GLOB, then this node is selected
if selector_part == SELECTOR_GLOB:
return True
# match package.node_name or package.dir.node_name
elif is_last and selector_part == real_node[-1]:
return True
elif len(real_node) <= i:
return False
elif real_node[i] == selector_part:
continue
else:
return False
# if we get all the way down here, then the node is a match
return True
def _node_is_match(
qualified_name: List[str], package_names: Set[str], fqn: List[str]
) -> bool:
"""Determine if a qualfied name matches an fqn, given the set of package
names in the graph.
:param List[str] qualified_name: The components of the selector or node
name, split on '.'.
:param Set[str] package_names: The set of pacakge names in the graph.
:param List[str] fqn: The node's fully qualified name in the graph.
def can_select_indirectly(node):
"""If a node is not selected itself, but its parent(s) are, it may qualify
for indirect selection.
Today, only Test nodes can be indirectly selected. In the future,
other node types or invocation flags might qualify.
"""
if len(qualified_name) == 1 and fqn[-1] == qualified_name[0]:
if node.resource_type == NodeType.Test:
return True
if qualified_name[0] in package_names:
if is_selected_node(fqn, qualified_name):
return True
for package_name in package_names:
local_qualified_node_name = [package_name] + qualified_name
if is_selected_node(fqn, local_qualified_node_name):
return True
return False
else:
return False
class ManifestSelector:
FILTER: str
def __init__(self, manifest):
self.manifest = manifest
def _node_iterator(
class NodeSelector(MethodManager):
"""The node selector is aware of the graph and manifest,
"""
def __init__(
self,
included_nodes: Set[str],
exclude: Optional[Container[str]],
include: Optional[Container[str]],
) -> Iterable[Tuple[str, str]]:
for unique_id, node in self.manifest.nodes.items():
if unique_id not in included_nodes:
continue
if include is not None and node.resource_type not in include:
continue
if exclude is not None and node.resource_type in exclude:
continue
yield unique_id, node
def parsed_nodes(self, included_nodes):
return self._node_iterator(
included_nodes,
exclude=(NodeType.Source,),
include=None)
def source_nodes(self, included_nodes):
return self._node_iterator(
included_nodes,
exclude=None,
include=(NodeType.Source,))
def search(self, included_nodes, selector):
raise NotImplementedError('subclasses should implement this')
class QualifiedNameSelector(ManifestSelector):
FILTER = SELECTOR_FILTERS.FQN
def search(self, included_nodes, selector):
"""Yield all nodes in the graph that match the selector.
:param str selector: The selector or node name
"""
qualified_name = selector.split(".")
package_names = {node.split(".")[1] for node in included_nodes}
for node, real_node in self.parsed_nodes(included_nodes):
if _node_is_match(qualified_name, package_names, real_node.fqn):
yield node
class TagSelector(ManifestSelector):
FILTER = SELECTOR_FILTERS.TAG
def search(self, included_nodes, selector):
""" yields nodes from graph that have the specified tag """
search = chain(self.parsed_nodes(included_nodes),
self.source_nodes(included_nodes))
for node, real_node in search:
if selector in real_node.tags:
yield node
class SourceSelector(ManifestSelector):
FILTER = SELECTOR_FILTERS.SOURCE
def search(self, included_nodes, selector):
"""yields nodes from graph are the specified source."""
parts = selector.split('.')
target_package = SELECTOR_GLOB
if len(parts) == 1:
target_source, target_table = parts[0], None
elif len(parts) == 2:
target_source, target_table = parts
elif len(parts) == 3:
target_package, target_source, target_table = parts
else: # len(parts) > 3 or len(parts) == 0
msg = (
'Invalid source selector value "{}". Sources must be of the '
'form `${{source_name}}`, '
'`${{source_name}}.${{target_name}}`, or '
'`${{package_name}}.${{source_name}}.${{target_name}}'
).format(selector)
raise dbt.exceptions.RuntimeException(msg)
for node, real_node in self.source_nodes(included_nodes):
if target_package not in (real_node.package_name, SELECTOR_GLOB):
continue
if target_source not in (real_node.source_name, SELECTOR_GLOB):
continue
if target_table in (None, real_node.name, SELECTOR_GLOB):
yield node
class InvalidSelectorError(Exception):
pass
ValidSelector = Union[QualifiedNameSelector, TagSelector, SourceSelector]
class MultiSelector:
"""The base class of the node selector. It only about the manifest and
selector types, including the glob operator, but does not handle any graph
related behavior.
"""
SELECTORS = [QualifiedNameSelector, TagSelector, SourceSelector]
def __init__(self, manifest):
self.manifest = manifest
def get_selector(
self, selector_type: str
graph: Graph,
manifest: Manifest,
previous_state: Optional[PreviousState] = None,
):
for cls in self.SELECTORS:
if cls.FILTER == selector_type:
return cls(self.manifest)
super().__init__(manifest, previous_state)
self.full_graph = graph
raise InvalidSelectorError(selector_type)
# build a subgraph containing only non-empty, enabled nodes and enabled
# sources.
graph_members = {
unique_id for unique_id in self.full_graph.nodes()
if self._is_graph_member(unique_id)
}
self.graph = self.full_graph.subgraph(graph_members)
def select_included(self, included_nodes, selector_type, selector_value):
selector = self.get_selector(selector_type)
return set(selector.search(included_nodes, selector_value))
def select_included(
self, included_nodes: Set[UniqueId], spec: SelectionCriteria,
) -> Set[UniqueId]:
"""Select the explicitly included nodes, using the given spec. Return
the selected set of unique IDs.
"""
method = self.get_method(spec.method, spec.method_arguments)
return set(method.search(included_nodes, spec.value))
def get_nodes_from_criteria(
self,
spec: SelectionCriteria
) -> Tuple[Set[UniqueId], Set[UniqueId]]:
"""Get all nodes specified by the single selection criteria.
class Graph:
"""A wrapper around the networkx graph that understands SelectionCriteria
and how they interact with the graph.
"""
def __init__(self, graph):
self.graph = graph
- collect the directly included nodes
- find their specified relatives
- perform any selector-specific expansion
"""
def nodes(self):
return set(self.graph.nodes())
nodes = self.graph.nodes()
try:
collected = self.select_included(nodes, spec)
except InvalidSelectorException:
valid_selectors = ", ".join(self.SELECTOR_METHODS)
logger.info(
f"The '{spec.method}' selector specified in {spec.raw} is "
f"invalid. Must be one of [{valid_selectors}]"
)
return set(), set()
def __iter__(self):
return iter(self.graph.nodes())
neighbors = self.collect_specified_neighbors(spec, collected)
direct_nodes, indirect_nodes = self.expand_selection(
selected=(collected | neighbors),
greedy=spec.greedy
)
return direct_nodes, indirect_nodes
def select_childrens_parents(self, selected: Set[str]) -> Set[str]:
ancestors_for = self.select_children(selected) | selected
return self.select_parents(ancestors_for) | ancestors_for
def collect_specified_neighbors(
self, spec: SelectionCriteria, selected: Set[UniqueId]
) -> Set[UniqueId]:
"""Given the set of models selected by the explicit part of the
selector (like "tag:foo"), apply the modifiers on the spec ("+"/"@").
Return the set of additional nodes that should be collected (which may
overlap with the selected set).
"""
additional: Set[UniqueId] = set()
if spec.childrens_parents:
additional.update(self.graph.select_childrens_parents(selected))
def select_children(self, selected: Set[str]) -> Set[str]:
descendants: Set[str] = set()
for node in selected:
descendants.update(nx.descendants(self.graph, node))
return descendants
if spec.parents:
depth = spec.parents_depth
additional.update(self.graph.select_parents(selected, depth))
def select_parents(self, selected: Set[str]) -> Set[str]:
ancestors: Set[str] = set()
for node in selected:
ancestors.update(nx.ancestors(self.graph, node))
return ancestors
def select_successors(self, selected: Set[str]) -> Set[str]:
successors: Set[str] = set()
for node in selected:
successors.update(self.graph.successors(node))
return successors
def collect_models(
self, selected: Set[str], spec: SelectionCriteria,
) -> Set[str]:
additional: Set[str] = set()
if spec.select_childrens_parents:
additional.update(self.select_childrens_parents(selected))
if spec.select_parents:
additional.update(self.select_parents(selected))
if spec.select_children:
additional.update(self.select_children(selected))
if spec.children:
depth = spec.children_depth
additional.update(self.graph.select_children(selected, depth))
return additional
def subgraph(self, nodes: Iterable[str]) -> 'Graph':
cls = type(self)
return cls(self.graph.subgraph(nodes))
def select_nodes_recursively(self, spec: SelectionSpec) -> Tuple[Set[UniqueId], Set[UniqueId]]:
"""If the spec is a composite spec (a union, difference, or intersection),
recurse into its selections and combine them. If the spec is a concrete
selection criteria, resolve that using the given graph.
"""
if isinstance(spec, SelectionCriteria):
direct_nodes, indirect_nodes = self.get_nodes_from_criteria(spec)
else:
bundles = [
self.select_nodes_recursively(component)
for component in spec
]
direct_sets = []
indirect_sets = []
class NodeSelector(MultiSelector):
def __init__(self, graph, manifest):
self.full_graph = Graph(graph)
super().__init__(manifest)
for direct, indirect in bundles:
direct_sets.append(direct)
indirect_sets.append(direct | indirect)
def get_nodes_from_spec(self, graph, spec):
try:
collected = self.select_included(graph.nodes(),
spec.selector_type,
spec.selector_value)
except InvalidSelectorError:
valid_selectors = ", ".join(s.FILTER for s in self.SELECTORS)
logger.info("The '{}' selector specified in {} is invalid. Must "
"be one of [{}]".format(
spec.selector_type,
spec.raw,
valid_selectors))
return set()
initial_direct = spec.combined(direct_sets)
indirect_nodes = spec.combined(indirect_sets)
specified = graph.collect_models(collected, spec)
collected.update(specified)
direct_nodes = self.incorporate_indirect_nodes(initial_direct, indirect_nodes)
tests = {
n for n in graph.select_successors(collected)
if self.manifest.nodes[n].resource_type == NodeType.Test
}
collected.update(tests)
if spec.expect_exists:
alert_non_existence(spec.raw, direct_nodes)
return collected
return direct_nodes, indirect_nodes
def select_nodes(self, graph, raw_include_specs, raw_exclude_specs):
selected_nodes: Set[str] = set()
def select_nodes(self, spec: SelectionSpec) -> Set[UniqueId]:
"""Select the nodes in the graph according to the spec.
for raw_spec in split_specs(raw_include_specs):
spec = SelectionCriteria(raw_spec)
included_nodes = self.get_nodes_from_spec(graph, spec)
selected_nodes.update(included_nodes)
This is the main point of entry for turning a spec into a set of nodes:
- Recurse through spec, select by criteria, combine by set operation
- Return final (unfiltered) selection set
"""
for raw_spec in split_specs(raw_exclude_specs):
spec = SelectionCriteria(raw_spec)
excluded_nodes = self.get_nodes_from_spec(graph, spec)
selected_nodes.difference_update(excluded_nodes)
direct_nodes, indirect_nodes = self.select_nodes_recursively(spec)
return direct_nodes
return selected_nodes
def _is_graph_member(self, node_name):
node = self.manifest.nodes[node_name]
if node.resource_type == NodeType.Source:
def _is_graph_member(self, unique_id: UniqueId) -> bool:
if unique_id in self.manifest.sources:
source = self.manifest.sources[unique_id]
return source.config.enabled
elif unique_id in self.manifest.exposures:
return True
node = self.manifest.nodes[unique_id]
return not node.empty and node.config.enabled
def _is_match(self, node_name, resource_types, tags, required):
node = self.manifest.nodes[node_name]
if node.resource_type not in resource_types:
return False
tags = set(tags)
if tags and not bool(set(node.tags) & tags):
# there are tags specified but none match
return False
for attr in required:
if not getattr(node, attr):
return False
def node_is_match(self, node: GraphMemberNode) -> bool:
"""Determine if a node is a match for the selector. Non-match nodes
will be excluded from results during filtering.
"""
return True
def get_selected(self, include, exclude, resource_types, tags, required):
include = coalesce(include, ['fqn:*', 'source:*'])
exclude = coalesce(exclude, [])
tags = coalesce(tags, [])
def _is_match(self, unique_id: UniqueId) -> bool:
node: GraphMemberNode
if unique_id in self.manifest.nodes:
node = self.manifest.nodes[unique_id]
elif unique_id in self.manifest.sources:
node = self.manifest.sources[unique_id]
elif unique_id in self.manifest.exposures:
node = self.manifest.exposures[unique_id]
else:
raise InternalException(
f'Node {unique_id} not found in the manifest!'
)
return self.node_is_match(node)
graph_members = {
node_name for node_name in self.full_graph.nodes()
if self._is_graph_member(node_name)
def filter_selection(self, selected: Set[UniqueId]) -> Set[UniqueId]:
"""Return the subset of selected nodes that is a match for this
selector.
"""
return {
unique_id for unique_id in selected if self._is_match(unique_id)
}
filtered_graph = self.full_graph.subgraph(graph_members)
selected_nodes = self.select_nodes(filtered_graph, include, exclude)
filtered_nodes = set()
for node_name in selected_nodes:
if self._is_match(node_name, resource_types, tags, required):
filtered_nodes.add(node_name)
def expand_selection(
self, selected: Set[UniqueId], greedy: bool = False
) -> Tuple[Set[UniqueId], Set[UniqueId]]:
# Test selection can expand to include an implicitly/indirectly selected test.
# In this way, `dbt test -m model_a` also includes tests that directly depend on `model_a`.
# Expansion has two modes, GREEDY and NOT GREEDY.
#
# GREEDY mode: If ANY parent is selected, select the test. We use this for EXCLUSION.
#
# NOT GREEDY mode:
# - If ALL parents are selected, select the test.
# - If ANY parent is missing, return it separately. We'll keep it around
# for later and see if its other parents show up.
# We use this for INCLUSION.
direct_nodes = set(selected)
indirect_nodes = set()
for unique_id in self.graph.select_successors(selected):
if unique_id in self.manifest.nodes:
node = self.manifest.nodes[unique_id]
if can_select_indirectly(node):
# should we add it in directly?
if greedy or set(node.depends_on.nodes) <= set(selected):
direct_nodes.add(unique_id)
# if not:
else:
indirect_nodes.add(unique_id)
return direct_nodes, indirect_nodes
def incorporate_indirect_nodes(
self, direct_nodes: Set[UniqueId], indirect_nodes: Set[UniqueId] = set()
) -> Set[UniqueId]:
# Check tests previously selected indirectly to see if ALL their
# parents are now present.
selected = set(direct_nodes)
for unique_id in indirect_nodes:
if unique_id in self.manifest.nodes:
node = self.manifest.nodes[unique_id]
if set(node.depends_on.nodes) <= set(selected):
selected.add(unique_id)
return selected
def get_selected(self, spec: SelectionSpec) -> Set[UniqueId]:
"""get_selected runs through the node selection process:
- node selection. Based on the include/exclude sets, the set
of matched unique IDs is returned
- expand the graph at each leaf node, before combination
- selectors might override this. for example, this is where
tests are added
- filtering:
- selectors can filter the nodes after all of them have been
selected
"""
selected_nodes = self.select_nodes(spec)
filtered_nodes = self.filter_selection(selected_nodes)
return filtered_nodes
def select(self, query):
include = query.get('include')
exclude = query.get('exclude')
resource_types = query.get('resource_types')
tags = query.get('tags')
required = query.get('required', ())
addin_ephemeral_nodes = query.get('addin_ephemeral_nodes', True)
def get_graph_queue(self, spec: SelectionSpec) -> GraphQueue:
"""Returns a queue over nodes in the graph that tracks progress of
dependecies.
"""
selected_nodes = self.get_selected(spec)
new_graph = self.full_graph.get_subset_graph(selected_nodes)
# should we give a way here for consumers to mutate the graph?
return GraphQueue(new_graph.graph, self.manifest, selected_nodes)
selected = self.get_selected(include, exclude, resource_types, tags,
required)
# if you haven't selected any nodes, return that so we can give the
# nice "no models selected" message.
if not selected:
return selected
class ResourceTypeSelector(NodeSelector):
def __init__(
self,
graph: Graph,
manifest: Manifest,
previous_state: Optional[PreviousState],
resource_types: List[NodeType],
):
super().__init__(
graph=graph,
manifest=manifest,
previous_state=previous_state,
)
self.resource_types: Set[NodeType] = set(resource_types)
# we used to carefully go through all node ancestors and add those if
# they were ephemeral. Sadly, the algorithm we used ended up being
# O(n^2). Instead, since ephemeral nodes are almost free, just add all
# ephemeral nodes in the graph.
# someday at large enough scale we might want to prune it to only be
# ancestors of the selected nodes so we can skip the compile.
if addin_ephemeral_nodes:
addins = {
uid for uid, node in self.manifest.nodes.items()
if node.is_ephemeral_model
}
else:
addins = set()
return selected | addins
def node_is_match(self, node):
return node.resource_type in self.resource_types

View File

@@ -0,0 +1,539 @@
import abc
from itertools import chain
from pathlib import Path
from typing import Set, List, Dict, Iterator, Tuple, Any, Union, Type, Optional
from dbt.dataclass_schema import StrEnum
from .graph import UniqueId
from dbt.contracts.graph.compiled import (
CompiledDataTestNode,
CompiledSchemaTestNode,
CompileResultNode,
ManifestNode,
)
from dbt.contracts.graph.manifest import Manifest, WritableManifest
from dbt.contracts.graph.parsed import (
HasTestMetadata,
ParsedDataTestNode,
ParsedExposure,
ParsedSchemaTestNode,
ParsedSourceDefinition,
)
from dbt.contracts.state import PreviousState
from dbt.exceptions import (
InternalException,
RuntimeException,
)
from dbt.node_types import NodeType
SELECTOR_GLOB = '*'
SELECTOR_DELIMITER = ':'
class MethodName(StrEnum):
FQN = 'fqn'
Tag = 'tag'
Source = 'source'
Path = 'path'
Package = 'package'
Config = 'config'
TestName = 'test_name'
TestType = 'test_type'
ResourceType = 'resource_type'
State = 'state'
Exposure = 'exposure'
def is_selected_node(fqn: List[str], node_selector: str):
# If qualified_name exactly matches model name (fqn's leaf), return True
if fqn[-1] == node_selector:
return True
# Flatten node parts. Dots in model names act as namespace separators
flat_fqn = [item for segment in fqn for item in segment.split('.')]
# Selector components cannot be more than fqn's
if len(flat_fqn) < len(node_selector.split('.')):
return False
for i, selector_part in enumerate(node_selector.split('.')):
# if we hit a GLOB, then this node is selected
if selector_part == SELECTOR_GLOB:
return True
elif flat_fqn[i] == selector_part:
continue
else:
return False
# if we get all the way down here, then the node is a match
return True
SelectorTarget = Union[ParsedSourceDefinition, ManifestNode, ParsedExposure]
class SelectorMethod(metaclass=abc.ABCMeta):
def __init__(
self,
manifest: Manifest,
previous_state: Optional[PreviousState],
arguments: List[str]
):
self.manifest: Manifest = manifest
self.previous_state = previous_state
self.arguments: List[str] = arguments
def parsed_nodes(
self,
included_nodes: Set[UniqueId]
) -> Iterator[Tuple[UniqueId, ManifestNode]]:
for key, node in self.manifest.nodes.items():
unique_id = UniqueId(key)
if unique_id not in included_nodes:
continue
yield unique_id, node
def source_nodes(
self,
included_nodes: Set[UniqueId]
) -> Iterator[Tuple[UniqueId, ParsedSourceDefinition]]:
for key, source in self.manifest.sources.items():
unique_id = UniqueId(key)
if unique_id not in included_nodes:
continue
yield unique_id, source
def exposure_nodes(
self,
included_nodes: Set[UniqueId]
) -> Iterator[Tuple[UniqueId, ParsedExposure]]:
for key, exposure in self.manifest.exposures.items():
unique_id = UniqueId(key)
if unique_id not in included_nodes:
continue
yield unique_id, exposure
def all_nodes(
self,
included_nodes: Set[UniqueId]
) -> Iterator[Tuple[UniqueId, SelectorTarget]]:
yield from chain(self.parsed_nodes(included_nodes),
self.source_nodes(included_nodes),
self.exposure_nodes(included_nodes))
def configurable_nodes(
self,
included_nodes: Set[UniqueId]
) -> Iterator[Tuple[UniqueId, CompileResultNode]]:
yield from chain(self.parsed_nodes(included_nodes),
self.source_nodes(included_nodes))
def non_source_nodes(
self,
included_nodes: Set[UniqueId],
) -> Iterator[Tuple[UniqueId, Union[ParsedExposure, ManifestNode]]]:
yield from chain(self.parsed_nodes(included_nodes),
self.exposure_nodes(included_nodes))
@abc.abstractmethod
def search(
self,
included_nodes: Set[UniqueId],
selector: str,
) -> Iterator[UniqueId]:
raise NotImplementedError('subclasses should implement this')
class QualifiedNameSelectorMethod(SelectorMethod):
def node_is_match(self, qualified_name: str, fqn: List[str]) -> bool:
"""Determine if a qualified name matches an fqn for all package
names in the graph.
:param str qualified_name: The qualified name to match the nodes with
:param List[str] fqn: The node's fully qualified name in the graph.
"""
unscoped_fqn = fqn[1:]
if is_selected_node(fqn, qualified_name):
return True
# Match nodes across different packages
elif is_selected_node(unscoped_fqn, qualified_name):
return True
return False
def search(
self, included_nodes: Set[UniqueId], selector: str
) -> Iterator[UniqueId]:
"""Yield all nodes in the graph that match the selector.
:param str selector: The selector or node name
"""
parsed_nodes = list(self.parsed_nodes(included_nodes))
for node, real_node in parsed_nodes:
if self.node_is_match(selector, real_node.fqn):
yield node
class TagSelectorMethod(SelectorMethod):
def search(
self, included_nodes: Set[UniqueId], selector: str
) -> Iterator[UniqueId]:
""" yields nodes from included that have the specified tag """
for node, real_node in self.all_nodes(included_nodes):
if selector in real_node.tags:
yield node
class SourceSelectorMethod(SelectorMethod):
def search(
self, included_nodes: Set[UniqueId], selector: str
) -> Iterator[UniqueId]:
"""yields nodes from included are the specified source."""
parts = selector.split('.')
target_package = SELECTOR_GLOB
if len(parts) == 1:
target_source, target_table = parts[0], None
elif len(parts) == 2:
target_source, target_table = parts
elif len(parts) == 3:
target_package, target_source, target_table = parts
else: # len(parts) > 3 or len(parts) == 0
msg = (
'Invalid source selector value "{}". Sources must be of the '
'form `${{source_name}}`, '
'`${{source_name}}.${{target_name}}`, or '
'`${{package_name}}.${{source_name}}.${{target_name}}'
).format(selector)
raise RuntimeException(msg)
for node, real_node in self.source_nodes(included_nodes):
if target_package not in (real_node.package_name, SELECTOR_GLOB):
continue
if target_source not in (real_node.source_name, SELECTOR_GLOB):
continue
if target_table not in (None, real_node.name, SELECTOR_GLOB):
continue
yield node
class ExposureSelectorMethod(SelectorMethod):
def search(
self, included_nodes: Set[UniqueId], selector: str
) -> Iterator[UniqueId]:
parts = selector.split('.')
target_package = SELECTOR_GLOB
if len(parts) == 1:
target_name = parts[0]
elif len(parts) == 2:
target_package, target_name = parts
else:
msg = (
'Invalid exposure selector value "{}". Exposures must be of '
'the form ${{exposure_name}} or '
'${{exposure_package.exposure_name}}'
).format(selector)
raise RuntimeException(msg)
for node, real_node in self.exposure_nodes(included_nodes):
if target_package not in (real_node.package_name, SELECTOR_GLOB):
continue
if target_name not in (real_node.name, SELECTOR_GLOB):
continue
yield node
class PathSelectorMethod(SelectorMethod):
def search(
self, included_nodes: Set[UniqueId], selector: str
) -> Iterator[UniqueId]:
"""Yields nodes from inclucded that match the given path.
"""
# use '.' and not 'root' for easy comparison
root = Path.cwd()
paths = set(p.relative_to(root) for p in root.glob(selector))
for node, real_node in self.all_nodes(included_nodes):
if Path(real_node.root_path) != root:
continue
ofp = Path(real_node.original_file_path)
if ofp in paths:
yield node
elif any(parent in paths for parent in ofp.parents):
yield node
class PackageSelectorMethod(SelectorMethod):
def search(
self, included_nodes: Set[UniqueId], selector: str
) -> Iterator[UniqueId]:
"""Yields nodes from included that have the specified package"""
for node, real_node in self.all_nodes(included_nodes):
if real_node.package_name == selector:
yield node
def _getattr_descend(obj: Any, attrs: List[str]) -> Any:
value = obj
for attr in attrs:
try:
value = getattr(value, attr)
except AttributeError:
# if it implements getitem (dict, list, ...), use that. On failure,
# raise an attribute error instead of the KeyError, TypeError, etc.
# that arbitrary getitem calls might raise
try:
value = value[attr]
except Exception as exc:
raise AttributeError(
f"'{type(value)}' object has no attribute '{attr}'"
) from exc
return value
class CaseInsensitive(str):
def __eq__(self, other):
if isinstance(other, str):
return self.upper() == other.upper()
else:
return self.upper() == other
class ConfigSelectorMethod(SelectorMethod):
def search(
self,
included_nodes: Set[UniqueId],
selector: Any,
) -> Iterator[UniqueId]:
parts = self.arguments
# special case: if the user wanted to compare test severity,
# make the comparison case-insensitive
if parts == ['severity']:
selector = CaseInsensitive(selector)
# search sources is kind of useless now source configs only have
# 'enabled', which you can't really filter on anyway, but maybe we'll
# add more someday, so search them anyway.
for node, real_node in self.configurable_nodes(included_nodes):
try:
value = _getattr_descend(real_node.config, parts)
except AttributeError:
continue
else:
if selector == value:
yield node
class ResourceTypeSelectorMethod(SelectorMethod):
def search(
self, included_nodes: Set[UniqueId], selector: str
) -> Iterator[UniqueId]:
try:
resource_type = NodeType(selector)
except ValueError as exc:
raise RuntimeException(
f'Invalid resource_type selector "{selector}"'
) from exc
for node, real_node in self.parsed_nodes(included_nodes):
if real_node.resource_type == resource_type:
yield node
class TestNameSelectorMethod(SelectorMethod):
def search(
self, included_nodes: Set[UniqueId], selector: str
) -> Iterator[UniqueId]:
for node, real_node in self.parsed_nodes(included_nodes):
if isinstance(real_node, HasTestMetadata):
if real_node.test_metadata.name == selector:
yield node
class TestTypeSelectorMethod(SelectorMethod):
def search(
self, included_nodes: Set[UniqueId], selector: str
) -> Iterator[UniqueId]:
search_types: Tuple[Type, ...]
if selector == 'schema':
search_types = (ParsedSchemaTestNode, CompiledSchemaTestNode)
elif selector == 'data':
search_types = (ParsedDataTestNode, CompiledDataTestNode)
else:
raise RuntimeException(
f'Invalid test type selector {selector}: expected "data" or '
'"schema"'
)
for node, real_node in self.parsed_nodes(included_nodes):
if isinstance(real_node, search_types):
yield node
class StateSelectorMethod(SelectorMethod):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.modified_macros: Optional[List[str]] = None
def _macros_modified(self) -> List[str]:
# we checked in the caller!
if self.previous_state is None or self.previous_state.manifest is None:
raise InternalException(
'No comparison manifest in _macros_modified'
)
old_macros = self.previous_state.manifest.macros
new_macros = self.manifest.macros
modified = []
for uid, macro in new_macros.items():
if uid in old_macros:
old_macro = old_macros[uid]
if macro.macro_sql != old_macro.macro_sql:
modified.append(uid)
else:
modified.append(uid)
for uid, macro in old_macros.items():
if uid not in new_macros:
modified.append(uid)
return modified
def recursively_check_macros_modified(self, node):
# check if there are any changes in macros the first time
if self.modified_macros is None:
self.modified_macros = self._macros_modified()
# loop through all macros that this node depends on
for macro_uid in node.depends_on.macros:
# is this macro one of the modified macros?
if macro_uid in self.modified_macros:
return True
# if not, and this macro depends on other macros, keep looping
macro = self.manifest.macros[macro_uid]
if len(macro.depends_on.macros) > 0:
return self.recursively_check_macros_modified(macro)
else:
return False
return False
def check_modified(self, old: Optional[SelectorTarget], new: SelectorTarget) -> bool:
different_contents = not new.same_contents(old) # type: ignore
upstream_macro_change = self.recursively_check_macros_modified(new)
return different_contents or upstream_macro_change
def check_modified_body(self, old: Optional[SelectorTarget], new: SelectorTarget) -> bool:
if hasattr(new, "same_body"):
return not new.same_body(old) # type: ignore
else:
return False
def check_modified_configs(self, old: Optional[SelectorTarget], new: SelectorTarget) -> bool:
if hasattr(new, "same_config"):
return not new.same_config(old) # type: ignore
else:
return False
def check_modified_persisted_descriptions(
self, old: Optional[SelectorTarget], new: SelectorTarget
) -> bool:
if hasattr(new, "same_persisted_description"):
return not new.same_persisted_description(old) # type: ignore
else:
return False
def check_modified_relation(
self, old: Optional[SelectorTarget], new: SelectorTarget
) -> bool:
if hasattr(new, "same_database_representation"):
return not new.same_database_representation(old) # type: ignore
else:
return False
def check_modified_macros(self, _, new: SelectorTarget) -> bool:
return self.recursively_check_macros_modified(new)
def check_new(self, old: Optional[SelectorTarget], new: SelectorTarget) -> bool:
return old is None
def search(
self, included_nodes: Set[UniqueId], selector: str
) -> Iterator[UniqueId]:
if self.previous_state is None or self.previous_state.manifest is None:
raise RuntimeException(
'Got a state selector method, but no comparison manifest'
)
state_checks = {
# it's new if there is no old version
'new': lambda old, _: old is None,
# use methods defined above to compare properties of old + new
'modified': self.check_modified,
'modified.body': self.check_modified_body,
'modified.configs': self.check_modified_configs,
'modified.persisted_descriptions': self.check_modified_persisted_descriptions,
'modified.relation': self.check_modified_relation,
'modified.macros': self.check_modified_macros,
}
if selector in state_checks:
checker = state_checks[selector]
else:
raise RuntimeException(
f'Got an invalid selector "{selector}", expected one of '
f'"{list(state_checks)}"'
)
manifest: WritableManifest = self.previous_state.manifest
for node, real_node in self.all_nodes(included_nodes):
previous_node: Optional[SelectorTarget] = None
if node in manifest.nodes:
previous_node = manifest.nodes[node]
elif node in manifest.sources:
previous_node = manifest.sources[node]
elif node in manifest.exposures:
previous_node = manifest.exposures[node]
if checker(previous_node, real_node):
yield node
class MethodManager:
SELECTOR_METHODS: Dict[MethodName, Type[SelectorMethod]] = {
MethodName.FQN: QualifiedNameSelectorMethod,
MethodName.Tag: TagSelectorMethod,
MethodName.Source: SourceSelectorMethod,
MethodName.Path: PathSelectorMethod,
MethodName.Package: PackageSelectorMethod,
MethodName.Config: ConfigSelectorMethod,
MethodName.TestName: TestNameSelectorMethod,
MethodName.TestType: TestTypeSelectorMethod,
MethodName.State: StateSelectorMethod,
MethodName.Exposure: ExposureSelectorMethod,
}
def __init__(
self,
manifest: Manifest,
previous_state: Optional[PreviousState],
):
self.manifest = manifest
self.previous_state = previous_state
def get_method(
self, method: MethodName, method_arguments: List[str]
) -> SelectorMethod:
if method not in self.SELECTOR_METHODS:
raise InternalException(
f'Method name "{method}" is a valid node selection '
f'method name, but it is not handled'
)
cls: Type[SelectorMethod] = self.SELECTOR_METHODS[method]
return cls(self.manifest, self.previous_state, method_arguments)

View File

@@ -0,0 +1,212 @@
import os
import re
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass
from typing import (
Set, Iterator, List, Optional, Dict, Union, Any, Iterable, Tuple
)
from .graph import UniqueId
from .selector_methods import MethodName
from dbt.exceptions import RuntimeException, InvalidSelectorException
RAW_SELECTOR_PATTERN = re.compile(
r'\A'
r'(?P<childrens_parents>(\@))?'
r'(?P<parents>((?P<parents_depth>(\d*))\+))?'
r'((?P<method>([\w.]+)):)?(?P<value>(.*?))'
r'(?P<children>(\+(?P<children_depth>(\d*))))?'
r'\Z'
)
SELECTOR_METHOD_SEPARATOR = '.'
def _probably_path(value: str):
"""Decide if value is probably a path. Windows has two path separators, so
we should check both sep ('\\') and altsep ('/') there.
"""
if os.path.sep in value:
return True
elif os.path.altsep is not None and os.path.altsep in value:
return True
else:
return False
def _match_to_int(match: Dict[str, str], key: str) -> Optional[int]:
raw = match.get(key)
# turn the empty string into None, too.
if not raw:
return None
try:
return int(raw)
except ValueError as exc:
raise RuntimeException(
f'Invalid node spec - could not handle parent depth {raw}'
) from exc
SelectionSpec = Union[
'SelectionCriteria',
'SelectionIntersection',
'SelectionDifference',
'SelectionUnion',
]
@dataclass
class SelectionCriteria:
raw: Any
method: MethodName
method_arguments: List[str]
value: Any
childrens_parents: bool
parents: bool
parents_depth: Optional[int]
children: bool
children_depth: Optional[int]
greedy: bool = False
def __post_init__(self):
if self.children and self.childrens_parents:
raise RuntimeException(
f'Invalid node spec {self.raw} - "@" prefix and "+" suffix '
'are incompatible'
)
@classmethod
def default_method(cls, value: str) -> MethodName:
if _probably_path(value):
return MethodName.Path
else:
return MethodName.FQN
@classmethod
def parse_method(
cls, groupdict: Dict[str, Any]
) -> Tuple[MethodName, List[str]]:
raw_method = groupdict.get('method')
if raw_method is None:
return cls.default_method(groupdict['value']), []
method_parts: List[str] = raw_method.split(SELECTOR_METHOD_SEPARATOR)
try:
method_name = MethodName(method_parts[0])
except ValueError as exc:
raise InvalidSelectorException(
f"'{method_parts[0]}' is not a valid method name"
) from exc
method_arguments: List[str] = method_parts[1:]
return method_name, method_arguments
@classmethod
def selection_criteria_from_dict(
cls, raw: Any, dct: Dict[str, Any], greedy: bool = False
) -> 'SelectionCriteria':
if 'value' not in dct:
raise RuntimeException(
f'Invalid node spec "{raw}" - no search value!'
)
method_name, method_arguments = cls.parse_method(dct)
parents_depth = _match_to_int(dct, 'parents_depth')
children_depth = _match_to_int(dct, 'children_depth')
return cls(
raw=raw,
method=method_name,
method_arguments=method_arguments,
value=dct['value'],
childrens_parents=bool(dct.get('childrens_parents')),
parents=bool(dct.get('parents')),
parents_depth=parents_depth,
children=bool(dct.get('children')),
children_depth=children_depth,
greedy=greedy
)
@classmethod
def dict_from_single_spec(cls, raw: str, greedy: bool = False):
result = RAW_SELECTOR_PATTERN.match(raw)
if result is None:
return {'error': 'Invalid selector spec'}
dct: Dict[str, Any] = result.groupdict()
method_name, method_arguments = cls.parse_method(dct)
meth_name = str(method_name)
if method_arguments:
meth_name = meth_name + '.' + '.'.join(method_arguments)
dct['method'] = meth_name
dct = {k: v for k, v in dct.items() if (v is not None and v != '')}
if 'childrens_parents' in dct:
dct['childrens_parents'] = bool(dct.get('childrens_parents'))
if 'parents' in dct:
dct['parents'] = bool(dct.get('parents'))
if 'children' in dct:
dct['children'] = bool(dct.get('children'))
return dct
@classmethod
def from_single_spec(cls, raw: str, greedy: bool = False) -> 'SelectionCriteria':
result = RAW_SELECTOR_PATTERN.match(raw)
if result is None:
# bad spec!
raise RuntimeException(f'Invalid selector spec "{raw}"')
return cls.selection_criteria_from_dict(raw, result.groupdict(), greedy=greedy)
class BaseSelectionGroup(Iterable[SelectionSpec], metaclass=ABCMeta):
def __init__(
self,
components: Iterable[SelectionSpec],
expect_exists: bool = False,
raw: Any = None,
):
self.components: List[SelectionSpec] = list(components)
self.expect_exists = expect_exists
self.raw = raw
def __iter__(self) -> Iterator[SelectionSpec]:
for component in self.components:
yield component
@abstractmethod
def combine_selections(
self,
selections: List[Set[UniqueId]],
) -> Set[UniqueId]:
raise NotImplementedError(
'_combine_selections not implemented!'
)
def combined(self, selections: List[Set[UniqueId]]) -> Set[UniqueId]:
if not selections:
return set()
return self.combine_selections(selections)
class SelectionIntersection(BaseSelectionGroup):
def combine_selections(
self,
selections: List[Set[UniqueId]],
) -> Set[UniqueId]:
return set.intersection(*selections)
class SelectionDifference(BaseSelectionGroup):
def combine_selections(
self,
selections: List[Set[UniqueId]],
) -> Set[UniqueId]:
return set.difference(*selections)
class SelectionUnion(BaseSelectionGroup):
def combine_selections(
self,
selections: List[Set[UniqueId]],
) -> Set[UniqueId]:
return set.union(*selections)

View File

@@ -1,13 +1,28 @@
# never name this package "types", or mypy will crash in ugly ways
from dataclasses import dataclass
from datetime import timedelta
from typing import NewType, Dict
from pathlib import Path
from typing import Tuple, AbstractSet, Union
from hologram import (
FieldEncoder, JsonSchemaMixin, JsonDict, ValidationError
from dbt.dataclass_schema import (
dbtClassMixin, ValidationError, StrEnum,
)
from hologram import FieldEncoder, JsonDict
from mashumaro.types import SerializableType
Port = NewType('Port', int)
class Port(int, SerializableType):
@classmethod
def _deserialize(cls, value: Union[int, str]) -> 'Port':
try:
value = int(value)
except ValueError:
raise ValidationError(f'Cannot encode {value} into port number')
return Port(value)
def _serialize(self) -> int:
return self
class PortEncoder(FieldEncoder):
@@ -37,41 +52,44 @@ class TimeDeltaFieldEncoder(FieldEncoder[timedelta]):
return {'type': 'number'}
class NoValue:
"""Sometimes, you want a way to say none that isn't None"""
def __eq__(self, other):
return isinstance(other, NoValue)
class PathEncoder(FieldEncoder):
def to_wire(self, value: Path) -> str:
return str(value)
class NoValueEncoder(FieldEncoder):
# the FieldEncoder class specifies a narrow range that only includes value
# types (str, float, None) but we want to support something extra
def to_wire(self, value: NoValue) -> Dict[str, str]: # type: ignore
return {'novalue': 'novalue'}
def to_python(self, value) -> NoValue:
if (
not isinstance(value, dict) or
'novalue' not in value or
value['novalue'] != 'novalue'
):
raise ValidationError('Got invalid NoValue: {}'.format(value))
return NoValue()
def to_python(self, value) -> Path:
if isinstance(value, Path):
return value
try:
return Path(value)
except TypeError:
raise ValidationError(
'cannot encode {} into timedelta'.format(value)
) from None
@property
def json_schema(self):
return {
'type': 'object',
'properties': {
'novalue': {
'enum': ['novalue'],
}
}
}
def json_schema(self) -> JsonDict:
return {'type': 'string'}
JsonSchemaMixin.register_field_encoders({
class NVEnum(StrEnum):
novalue = 'novalue'
def __eq__(self, other):
return isinstance(other, NVEnum)
@dataclass
class NoValue(dbtClassMixin):
"""Sometimes, you want a way to say none that isn't None"""
novalue: NVEnum = NVEnum.novalue
dbtClassMixin.register_field_encoders({
Port: PortEncoder(),
timedelta: TimeDeltaFieldEncoder(),
NoValue: NoValueEncoder(),
Path: PathEncoder(),
})
FQNPath = Tuple[str, ...]
PathSet = AbstractSet[FQNPath]

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