Compare commits

...

70 Commits

Author SHA1 Message Date
Emily Rockman
734b6429c7 update to use Docs object 2022-07-27 13:34:23 -05:00
Benoit Perigaud
a75b2c0a90 Make docs a dataclass instead of a Dict 2022-07-26 19:38:22 +02:00
Benoit Perigaud
6b6ae22434 Merge branch 'feature/custom-node-colors-dbt_project' of github.com:dbt-labs/dbt-core into feature/custom-node-colors-dbt_project 2022-07-20 12:51:29 +02:00
Benoit Perigaud
287f443ec9 Make docs a Dict to avoid parsing errors 2022-07-20 12:44:01 +02:00
Benoit Perigaud
aea2c4a29b Add node_color to Docs 2022-07-20 12:43:09 +02:00
Benoit Perigaud
21ffe31270 Handle when docs is both under docs and config.docs 2022-07-20 12:42:25 +02:00
Sung Won Chung
70c9074625 Merge branch 'main' of https://github.com/dbt-labs/dbt into feature/custom-node-colors-dbt_project 2022-07-19 14:27:18 -05:00
Tomás Farías Santana
2048a1af6f [CT-472] feat: Retrying method for acquiring connection handles (#5432)
Add reusable function for retrying adapter connections. Utilize said function to add retries for Postgres (and Redshift).
2022-07-19 14:55:55 -04:00
Emily Rockman
71223dc253 add timeouts to integration tests (#5481)
* add timeout to integration tests

* add timeout to all jobs in workflow
2022-07-19 10:31:46 -05:00
Emily Rockman
e03d35a9fc Fix: Rename try methods to strict (#5477) (#5478)
* rename strict methods

* add changelog

Co-authored-by: Jeremy Yeo <jeremyyeo@users.noreply.github.com>
2022-07-15 09:38:21 -05:00
github-actions[bot]
f988f76fcc Bumping version to 1.3.0a1 (#5463)
* Bumping version to 1.3.0a1

* Remove whitespace

Co-authored-by: Github Build Bot <buildbot@fishtownanalytics.com>
Co-authored-by: leahwicz <60146280+leahwicz@users.noreply.github.com>
2022-07-12 14:17:06 -04:00
Emily Rockman
0cacfd0f88 cleanup changelog after cutting 1.2.latest branch (#5462) 2022-07-12 11:05:50 -05:00
Matthew McKnight
c25260e5dd Add Grant SQL Macros (#5369)
* init push or ct-660 work

* changes to default versions of get_show_grant_sql and get_grant_sql

* completing init default versions of all macros being called for look over and collaboration

* minor update to should_revoke

* post pairing push up (does have log statements to make sure we remove)

* minor spacing changes

* minor changes, and removal of logs so people can have clean grab of code

* minor changes to how get_revoke_sql works

* init attempt at applying apply_grants to all materialzations

* name change from recipents -> grantee

* minor changes

* working on making a context to handle the diff gathering between grant_config and curreent_grants to see what needs to be revoked, I know if we assign a role, and a model becomes dependent on it we can't drop the role now still not seeing the diff appear in log

* removing logs from most materializations to better track diff of grants generation logs

* starting to build out postgres get_show_grant_sql getting empty query errors hopefully will clear up as we add the other postgres versions of macros and isn't a psycopg2 issue as indicated by searching

* 6/27 eod update looking into diff_grants variable not getting passed into get_revoke_sql

* changes to loop cases

* changes after pairing meeting

* adding apply_grants to create_or_replace_view.sql

* models are building but testing out small issues around revoke statement never building

* postgrest must fixes from jeremy's feedback

* postgres minor change to standarize_grants_dict

* updating after pairing with dough and jeremey incorporating the new version of should revoke logic.

* adding  ref of diff_of_two_dicts to base keys ref

* change of method type for standardize_grants_dict

* minor update trying to fix unit test

* changes based on morning feedback

* change log message in default_apply_grants macro

* CT-808 grant adapter tests (#5447)

* Create initial test for grants

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

* rename grant[privilege] -> grant_config[privilege]

* postgres macro rename to copy_grants

* CT-808 more grant adapter tests (#5452)

* Add tests for invalid user and invalid privilege

* Add more grant tests

* Macro touch ups

* Many more tests

* Allow easily replacing privilege names

* Keep adding tests

* Refactor macros to return lists, fix test

* Code checks

* Keep tweaking tests

* Revert cool grantees join bc Snowflake isnt happy

* Use Postgres/BQ as standard for standardize_grants_dict

* Code checks

* add missing replace

* small replace tweak,  add additional dict diffs

* All tests passing on BQ

* Add type cast to test_snapshot_grants

* Refactor for DRYer apply_grants macros

Co-authored-by: Jeremy Cohen <jeremy@dbtlabs.com>
Co-authored-by: Emily Rockman <emily.rockman@dbtlabs.com>

* update to main, create changelog, whitespace fixes

Co-authored-by: Gerda Shank <gerda@dbtlabs.com>
Co-authored-by: Jeremy Cohen <jeremy@dbtlabs.com>
Co-authored-by: Emily Rockman <emily.rockman@dbtlabs.com>
2022-07-11 11:58:41 -05:00
Batuhan Taskaya
c521fa6b74 Add --target-path as a CLI option. (#5402) 2022-07-07 12:49:25 -04:00
Emily Rockman
f304b4b2da add new index file for dbt-docs for 1.2.0 release (#5446)
* add new index file for dbt-docs for 1.2.0 release

* add PR number to changelogs
2022-07-06 16:12:38 -05:00
Drew Banin
064d890172 (#4884) Add support for ratio metrics (#5027)
* wip

* More support for ratio metrics

* Formatting and linting

* Fix unit tests

* Support disabling metrics

* mypy

* address all TODOs

* make pypy happy

* wip

* checkpoint

* refactor, remove ratio_terms

* flake8 and unit tests

* remove debugger

* quickfix for filters

* Experiment with functional testing for 'expression' metrics

* reformatting slightly

* make file and mypy fix

* remove config from metrics - wip

* add metrics back to context

* adding test changes

* fixing test metrics

* revert name audit

* pre-commit fixes

* add changelog

* Bumping manifest version to v6 (#5430)

* Bumping manifest version to v6

* Adding manifest file for tests

* Reverting unneeded changes

* Updating v6

* Updating test to add metrics field

* Adding changelog

* add v5 to backwards compatibility

* Clean up test_previous_version_state, update for v6 (#5440)

* Update test_previous_version_state for v6. Cleanup

* Regenerate, rm breakpoint

* Code checks

* Add assertion that will fail when we bump manifest version

* update tests to automatically tests all previous versions

Co-authored-by: Emily Rockman <emily.rockman@dbtlabs.com>

Co-authored-by: Jeremy Cohen <jeremy@dbtlabs.com>
Co-authored-by: Callum McCann <cmccann51@gmail.com>
Co-authored-by: Emily Rockman <emily.rockman@dbtlabs.com>
Co-authored-by: leahwicz <60146280+leahwicz@users.noreply.github.com>
2022-07-06 16:01:14 -05:00
Paolo Quadri
febbd978b5 Fix/fix quotes behaviour in snapshots (#5389)
* fix: add quotes to relation

* fix: add quotes correctly

* chore: reset main.py

* chore: reset main.py from master

* chore: newline

* chore: changie
2022-07-06 16:30:19 -04:00
dependabot[bot]
0d7e87fac6 Bump mypy from 0.942 to 0.961 (#5337)
* Bump mypy from 0.942 to 0.961

Bumps [mypy](https://github.com/python/mypy) from 0.942 to 0.961.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.942...v0.961)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* Add automated changelog yaml from template

* Fixed deprecated abstractclassmethod caught by new version of mypy

* Fixed deprecated abstractclassmethod not caught by mypy

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Github Build Bot <buildbot@fishtownanalytics.com>
Co-authored-by: Ian Knox <ian.knox@dbtlabs.com>
2022-07-06 16:25:05 -04:00
Volkan Gürel
3500528506 Add inheritance to materialization macro resolution (#5348)
* Add inheritance to materialization macro resolution

* Add changelog entry

* Address PR feedback
2022-07-06 13:56:52 -04:00
Joe Markiewicz
c42221fcf3 feature/deps-regex-semver (#5370)
* feature/deps-regex-semver

* pre-commit fixes

* applying review updates
2022-07-06 10:56:07 -04:00
dependabot[bot]
f49f28c331 Update colorama requirement from <0.4.5,>=0.3.9 to >=0.3.9,<0.4.6 in /core (#5388)
* Update colorama requirement in /core
2022-07-05 14:46:56 -05:00
dependabot[bot]
dc964c43d9 Bump black from 22.3.0 to 22.6.0 (#5420)
* Bump black from 22.3.0 to 22.6.0
2022-07-05 14:02:15 -05:00
Peter Debelak
60e491b3c1 Improve pluralizations for Documentation and SqlOperation NodeTypes (#5356)
* Improve pluralizations for Documentation and SqlOperation NodeTypes

Previously these were `docss` and `sqlss` which leaves something to be
desired.

* Add changie changelog entry for pluralization change

* Slighly simplify node type pluralization tests

* Update node type names for sql and docs so they match pluralizations
2022-06-30 16:15:07 -04:00
Jeremy Cohen
3bfce2bac9 Move data type macros into dbt-core (#5428)
* Move data type macros into dbt-core

* Changelog entry

* Code quality checks. Fix type_float
2022-06-30 19:46:07 +02:00
Darin Douglass
d63ad4cd82 [CT-679] flip logic around reset color detection (#5394) 2022-06-29 17:28:01 -04:00
Jeremy Cohen
d5608dca32 Follow-up to #5334 (#5382)
* Follow-up to secret rendering changes

* Update changelog entries

* PR feedback
2022-06-28 10:06:12 +02:00
Mila Page
e7031f2d74 Ct 488/migrate invalid model tests (#5391)
Migrate 006 dependency tests to the new framework
2022-06-24 12:05:41 -04:00
github-actions[bot]
68a2996788 Bumping version to 1.2.0b1 (#5406)
* Bumping version to 1.2.0b1

* Add changelog

* Remove whitespace

Co-authored-by: Github Build Bot <buildbot@fishtownanalytics.com>
Co-authored-by: Leah Antkiewicz <leah.antkiewicz@fishtownanalytics.com>
Co-authored-by: leahwicz <60146280+leahwicz@users.noreply.github.com>
2022-06-24 10:10:39 -04:00
leahwicz
f5f0a7f908 Removing old script from bumpversion (#5405) 2022-06-24 09:42:48 -04:00
Jeremy Yeo
1cfc0851ca [Fix]: remove duplicate key checking (#5403)
* remove duplicate key checking

* add changelog
2022-06-23 15:24:15 -04:00
Benoit Perigaud
9fca33cb29 Add docs config and input validation 2022-06-21 09:10:15 +02:00
Benoit Perigaud
6360247d39 Remove node_color from the original docs config 2022-06-21 09:09:35 +02:00
Ulises Ojeda
d257d0b44c Early return from dbt init if no available adapters (#5366)
* Exit from dbt init if no available adapters

* adding chnagie change

* fixing init_tests
2022-06-17 09:42:37 -07:00
Matthew McKnight
f8d347e5f8 deleting scaffold and create_adater_plugin.py file from scripts section of core as they a… (#5117)
* deleting scaffold and .py file from scripts section of core as they are either deprecated or will live outside of core

* adding changelog

* removing files that shouldn't be there

* update changelog to have link to new scaffold

* readding the original script file but changing its output ot be a print statement and leave comment that also points to the new scaffold

* sentence change
2022-06-16 14:00:08 -05:00
Doug Beatty
a02db03f45 Lift + shift for cross-db macros (#5298)
* Initialize lift + shift, dateadd + datediff

* Placeholder changelog for now

* Lift and shift cross-database macros, fixtures, and tests from dbt-utils

* Switch namespace from `dbt_utils` to `dbt`

* Remove unreferenced variable

* Remove conflicting definition of current_timestamp()

* Trim leading and trailing whitespace

* Apply Black formatting

* Remove unused import

* Remove references to other profiles

* Update .changes/unreleased/Features-20220518-114604.yaml

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

* Kick out the `type_*` tests

* Kick out the `type_*` macros

* Kick out the `current_timestamp` tests

* Kick out the `current_timestamp` macros

* Kick out the `current_timestamp` macros

* Kick out the `type_*` macros

* Use built-in adapter functionality for converting string datatypes

* Move comment above macro for postgres__any_value

Co-authored-by: Jeremy Cohen <jeremy@dbtlabs.com>
2022-06-16 20:07:36 +02:00
Jeremy Cohen
6e8388c653 Declare compatibility for previous artifact versions (#5346)
* Add functional test

* Add is_compatible_version logic

* Add changelog entry

* Fix mypy
2022-06-16 12:19:18 +02:00
jared-rimmer
6572b7e0a5 Prettify duration message at end of execution (#5364)
* Prettify duration message at end of execution
2022-06-15 12:28:55 -07:00
Matthew McKnight
26bb5c3484 Adapters README (doc days of winter) (#5332)
* init docs write up

* updating based on feedback from gerda

* minor sentence structure changes

* minor changes after feedback
2022-06-15 10:18:07 -05:00
leahwicz
83f4992073 Adding scheduled CI testing Action (#5349)
* Adding scheduled CI testing Action

* Fixing malformed message

* Fixing messaging quotes

* Update to not fail fast

* Reordered branches

* Updating job name

* Removed PR trigger used for testing
2022-06-14 13:24:52 -04:00
dependabot[bot]
8392023e9f Bump python from 3.10.3-slim-bullseye to 3.10.5-slim-bullseye in /docker (#5367)
* Bump python from 3.10.3-slim-bullseye to 3.10.5-slim-bullseye in /docker

Bumps python from 3.10.3-slim-bullseye to 3.10.5-slim-bullseye.

---
updated-dependencies:
- dependency-name: python
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* Add automated changelog yaml from template

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Github Build Bot <buildbot@fishtownanalytics.com>
2022-06-14 11:34:42 -04:00
Matthew McKnight
309efaa141 remove script for snowflake oauth reset as its been moved to snowflake (#5362)
* remove script for snowflake oauth reset as its been moved to snowflake

* changelog entry
2022-06-14 09:45:40 -05:00
Chenyu Li
a5993fc866 lower networkx for py37 (#5264) 2022-06-13 12:13:32 -07:00
Chenyu Li
5b1bc72ae1 python model adr (#5345)
* add python model adr first version
2022-06-13 07:54:55 -07:00
Greg Roodt
72b6a80b07 Remove duplicate dbt script entry (#5304)
* Remove duplicate dbt script entry.

* changie
2022-06-08 16:14:37 -07:00
Matt Winkler
f0fbb0e551 add Optional node_color config in Docs dataclass 2022-06-07 09:17:15 -06:00
leahwicz
e48f7ab32e Fixing Windows color regression (#5327)
* Fixing Windows color regression

* Cleaning up logic

* Consolidating logic to the logger

* Cleaning up vars

* Updating comment

* Removing unused import

* Fixing whitespace

* Adding changelog
2022-06-07 09:06:02 -04:00
Jeremy Cohen
16dc2be556 Update context readme, small code cleanup (#5334) 2022-06-06 23:39:34 +02:00
Ian Knox
eea872c319 New ancestor/descendant algo (#5326)
* New ancestor/descendant algo

* changelog
2022-06-02 10:46:01 -07:00
Gerda Shank
189c06dbb1 CT-710 fix test graph selection (#5324)
* Fix test_graph_selection so it can run separately

* Changie
2022-06-01 12:01:10 -04:00
Kyle Wigley
74662d1527 fix pip upgrade step in CI for windows (#5320)
* fix pip upgrade step in CI for windows

* Changie

Co-authored-by: Gerda Shank <gerda@dbtlabs.com>
2022-06-01 11:24:05 -04:00
Mila Page
75f3e8cb74 Add notes on invoking Python methods in adapter macros (#5306)
Co-authored-by: Mila Page <versusfacit@users.noreply.github.com>
2022-05-31 08:55:30 -07:00
Jeremy Cohen
aeee1c23a6 Tidying up materialization logic: relation names, cache loading, etc (#5221)
* Clean up existing_relation, load_relation,  etc

* Add changelog entry

* Empty commit, retrigger CI
2022-05-20 18:02:14 +02:00
Gerda Shank
e50678c914 Ct-581 grants as configs (#5230)
* Handle 'grants' in NodeConfig, with correct merge behavior

* Fix a bunch of tests

* Add changie

* Actually add the test

* Change to default replace of grants with '+' extending them

* Additional tests, fix config_call_dict handling

* Tweak _add_config_call to remove unnecessary isinstance checks
2022-05-19 14:50:47 -04:00
Josh Wills
ae62f5708c Switch the Makefile to use the default installed Python version instead of py38 for local testing (#5269)
* Setting up an env var to use to override the tox python variable used for local dev

* Switch over to py-based tox envs instead of the py38 ones to be friendly to dbt-core devs who don't work at dbt Labs

* changie
2022-05-19 12:50:26 -04:00
Gerda Shank
cda88d1948 CT-159 Remove docs file from manifest when deleting doc node (#5270)
* Convert partial parsing docs tests

* Failing test

* Remove doc file from manifest when doc node is removed

* Changie
2022-05-19 11:37:45 -04:00
Elize Papineau
e7218d3e99 Truncate relation names when appending a suffix (#4921)
* Truncate relation names when appending a suffix that will result in len > 63 characters using make_temp_relation and make_backup_relation macros

* Remove timestamp from suffix appended to backup relation

* Add changelog entry

* Implememt make_relation_with_suffix macro

* Add make_intermediate_relation macro that controls _tmp relation creation in table and view materializations to delienate from database- and schema-less behavior of relation returned from make_temp_relation

* Create backup_relation at top of materialization to use for identifier

* cleanup

* Add dstring arg to make_relation_with_suffix macro

* Only reference dstring in conditional of make_relation_with_suffix macro

* Create both a temp and intermediate relation, update preexisting_temp_relation to preexisting_intermediate_relation

* Migrate test updates to new test location

* Remove restored tmp.csv

* Revert "Remove restored tmp.csv"

This reverts commit 900c9dbcad9a1e6a5a6737c84004504bfdd9926f.

* Actually remove restored tmp.csv
2022-05-19 13:57:15 +02:00
leahwicz
2c42fb436c Creating ADR for versioning and branching strategy (#4998)
* Creating ADR for versioning and branching strategy

* Fixing image link

* Grammar clean-up

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>

* Grammar clean-up

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>

* Update docs/arch/adr-003-versioning-branching-strategy.md

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>

* Update docs/arch/adr-003-versioning-branching-strategy.md

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>

* Update docs/arch/adr-003-versioning-branching-strategy.md

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>

* Update docs/arch/adr-003-versioning-branching-strategy.md

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>

* Update docs/arch/adr-003-versioning-branching-strategy.md

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>

* Update docs/arch/adr-003-versioning-branching-strategy.md

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>

* Update docs/arch/adr-003-versioning-branching-strategy.md

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>

* Update docs/arch/adr-003-versioning-branching-strategy.md

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>

* Updating Outside Scope section

* Changing from using type to stage

* Adding section on getting changes into certain releases

* Changed stages to phases

* Some wording updates

* New section for branching pros and cons

* Clarifying version bump statement

* A few minor comment fix ups

* Adding requirement to define released

* Updating to completed!

Co-authored-by: Stu Kilgore <stuart.kilgore@gmail.com>
2022-05-18 23:12:20 -04:00
Gerda Shank
a9e1a0e00a Tweak test to avoid set ordering problem (#5272) 2022-05-18 17:28:48 -04:00
Jeremy Cohen
0d8e061a3d Add dbt Core roadmap as of May 2022 (#5246) 2022-05-15 10:47:55 +02:00
Stu Kilgore
7532420eef Fix macro modified from previous state with pkg (#5224)
* Fix macro modified from previous state with pkg

When iterating through nodes to check if any of its macro dependencies
have been modified, the state selector will first check all upstream
macro dependencies before returning a judgement.
2022-05-13 13:04:15 -05:00
Josh Wills
03b17ff401 Add support for File Selectors and add file selectors to the default method selector list (#5241)
* Add a new selector method for files and add it to the default method selection criteria if the given selector has a . in it but no path separators

* Add a file: selector method to the default selector methods because it will make Pedram happy

* changie stuff
2022-05-13 09:39:55 -07:00
Tomás Farías Santana
fc1fc2d5e9 fix: Avoid access to profile when calling str(UnsetProfileConfig) (#5209)
* fix: Avoid access to profile when calling str(UnsetProfileConfig)

dbt.config.UnsetProfileConfig inherits __str__ from
dbt.config.Project. Moreover, UnsetProfileConfig also raises an
exception when attempting to access unset profile attributes. As
Project.__str__ ultimately calls to_project_config and accesses said
profile attributes, we override to_project_config in
UnsetProfileConfig to avoid accessing the attributes that raise an
exception.

This allows calling str(UnsetProfileConfig) and
repr(UnsetProfileConfig).

Basic unit testing is also included in commit.

* fix: Skip repr for profile fields in UnsetProfileConfig

* chore(changie): Add changie file
2022-05-13 08:36:13 -07:00
Jeremy Yeo
7e43f36bb1 Feature: Add set/zip function to contexts (#5107)
* add set function to contexts

* add zip function to contexts

* add changelog

* add try_ equivalents

* remove defaults

* add tests

* update tests
2022-05-13 10:25:51 -05:00
Jeremy Cohen
72c17c4464 Fix: use adapter.get_columns_in_relation (#5232) 2022-05-11 16:48:25 +02:00
Jeremy Cohen
3996a69861 Fix: column comparison logic for check-strategy snapshots (#5223)
* Add test case

* Update comparison in snapshot_check_all_get_existing_columns

* Add changelog entry
2022-05-10 11:04:56 +02:00
leahwicz
aa8115aa5e Bumping hologram version (#5218)
* Bumping hologram version

* Add automated changelog yaml from template

* Updating issue

* Loosen requirement range

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

Co-authored-by: Github Build Bot <buildbot@fishtownanalytics.com>
Co-authored-by: Jeremy Cohen <jeremy@dbtlabs.com>
2022-05-09 15:49:11 -04:00
Ian Knox
ab0c3510eb Revert inaccurate mypy change (#5216) 2022-05-06 09:21:48 -05:00
Ian Knox
4480d05cfb Update mypy to latest and turn on everywhere (#5171) 2022-05-05 12:00:11 -05:00
Gerda Shank
788694ec5b Convert materialization tests to new test framework (#5212) 2022-05-04 13:55:09 -04:00
Nicolas Parot Alvarez
fb5bb7fff3 Improve tracking error logging message (#5211) 2022-05-04 11:00:13 -04:00
299 changed files with 14927 additions and 3573 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.2.0a1
current_version = 1.3.0a1
parse = (?P<major>\d+)
\.(?P<minor>\d+)
\.(?P<patch>\d+)
@@ -28,8 +28,6 @@ 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/postgres/dbt/adapters/postgres/__version__.py]

View File

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

View File

@@ -1,7 +0,0 @@
kind: Dependencies
body: "Bump ubuntu from 20.04 to 22.04"
time: 2022-04-27T19:51:28.000000-05:00
custom:
Author: dependabot[bot]
Issue: "4904"
PR: "5141"

View File

@@ -1,7 +0,0 @@
kind: Features
body: Add selector method when reading selector definitions
time: 2022-04-08T11:26:10.713088+10:00
custom:
Author: danieldiamond
Issue: "4821"
PR: "4827"

View File

@@ -1,7 +0,0 @@
kind: Features
body: Adds itertools to modules Jinja namespace
time: 2022-04-24T13:26:55.008246+01:00
custom:
Author: bd3dowling
Issue: "5130"
PR: "5140"

View File

@@ -1,7 +0,0 @@
kind: Features
body: allow target as an option in profile_template.yml
time: 2022-04-28T06:56:44.511519-04:00
custom:
Author: alexrosenfeld10
Issue: "5179"
PR: "5184"

View File

@@ -1,7 +0,0 @@
kind: Features
body: 'seed: Add new macro get_csv_sql'
time: 2022-05-03T14:29:34.847959075Z
custom:
Author: adamantike
Issue: "5206"
PR: "5207"

View File

@@ -0,0 +1,8 @@
kind: Features
body: Add reusable function for retrying adapter connections. Utilize said function
to add retries for Postgres (and Redshift).
time: 2022-07-15T03:55:55.270637265+02:00
custom:
Author: tomasfarias
Issue: "5022"
PR: "5432"

View File

@@ -1,7 +0,0 @@
kind: Fixes
body: Adding new cols to check_cols in snapshots
time: 2022-03-17T21:09:16.977086+01:00
custom:
Author: GtheSheep
Issue: "3146"
PR: "4893"

View File

@@ -1,7 +0,0 @@
kind: Fixes
body: Restore ability to utilize `updated_at` for check_cols snapshots
time: 2022-04-15T11:29:27.063462-06:00
custom:
Author: dbeatty10
Issue: "5076"
PR: "5077"

View File

@@ -1,7 +0,0 @@
kind: Fixes
body: Fix retry logic to return values after initial try
time: 2022-04-22T13:12:27.239055-05:00
custom:
Author: emmyoop
Issue: "5023"
PR: "5137"

View File

@@ -1,7 +0,0 @@
kind: Fixes
body: Use yaml renderer (with target context) for rendering selectors
time: 2022-04-22T13:56:45.147893-04:00
custom:
Author: gshank
Issue: "5131"
PR: "5136"

View File

@@ -1,7 +0,0 @@
kind: Fixes
body: Scrub secret env vars from CommandError in exception stacktrace
time: 2022-04-25T20:39:24.365495+02:00
custom:
Author: jtcohen6
Issue: "5151"
PR: "5152"

View File

@@ -1,7 +0,0 @@
kind: Fixes
body: Ensure the metric name does not contain spaces
time: 2022-04-26T20:21:04.360693-04:00
custom:
Author: gshank
Issue: "4572"
PR: "5173"

View File

@@ -1,7 +0,0 @@
kind: Fixes
body: When parsing 'all_sources' should be a list of unique dirs
time: 2022-04-27T10:26:48.648388-04:00
custom:
Author: gshank
Issue: "5120"
PR: "5176"

View File

@@ -1,7 +0,0 @@
kind: Fixes
body: Add warning if yaml contains duplicate keys
time: 2022-04-28T10:01:57.893956+12:00
custom:
Author: jeremyyeo
Issue: "5114"
PR: "5146"

View File

@@ -1,8 +0,0 @@
kind: Fixes
body: Modifying the drop_test_schema to work better with Redshift issues around locked
tables and current transactions
time: 2022-04-29T16:07:42.750046-05:00
custom:
Author: Mcknight-42
Issue: "5200"
PR: "5198"

View File

@@ -0,0 +1,7 @@
kind: Fixes
body: Rename try to strict for more intuitiveness
time: 2022-07-15T23:11:48.327928+12:00
custom:
Author: jeremyyeo
Issue: "5475"
PR: "5477"

View File

@@ -1,7 +0,0 @@
kind: Under the Hood
body: Migrating 005_simple_seed to the new test framework.
time: 2022-04-09T04:05:39.20045-07:00
custom:
Author: versusfacit
Issue: "200"
PR: "5013"

View File

@@ -1,7 +0,0 @@
kind: Under the Hood
body: Convert 029_docs_generate tests to new framework
time: 2022-04-13T18:30:14.706391-04:00
custom:
Author: gshank
Issue: "5035"
PR: "5058"

View File

@@ -1,7 +0,0 @@
kind: Under the Hood
body: Move package deprecation check outside of package cache
time: 2022-04-14T13:22:06.157579-05:00
custom:
Author: emmyoop
Issue: "5068"
PR: "5069"

View File

@@ -1,7 +0,0 @@
kind: Under the Hood
body: Converted dbt list tests to pytest
time: 2022-04-27T14:06:28.882908-05:00
custom:
Author: stu-k
Issue: "5049"
PR: "5178"

View File

@@ -14,6 +14,7 @@ kinds:
- label: Docs
- label: Under the Hood
- label: Dependencies
- label: Security
custom:
- key: Author
label: GitHub Username(s) (separated by a single space if multiple)

View File

@@ -38,6 +38,7 @@ jobs:
name: code-quality
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out the repository
@@ -48,13 +49,14 @@ jobs:
- name: Install python dependencies
run: |
pip install --user --upgrade pip
pip --version
pip install pre-commit
python -m pip install --user --upgrade pip
python -m pip --version
python -m pip install pre-commit
pre-commit --version
pip install mypy==0.782
python -m pip install mypy==0.942
mypy --version
pip install -r editable-requirements.txt
python -m pip install -r requirements.txt
python -m pip install -r dev-requirements.txt
dbt --version
- name: Run pre-commit hooks
@@ -64,11 +66,12 @@ jobs:
name: unit test / python ${{ matrix.python-version }}
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
python-version: ["3.7", "3.8", "3.9", "3.10"]
env:
TOXENV: "unit"
@@ -85,9 +88,9 @@ jobs:
- name: Install python dependencies
run: |
pip install --user --upgrade pip
pip --version
pip install tox
python -m pip install --user --upgrade pip
python -m pip --version
python -m pip install tox
tox --version
- name: Run tox
@@ -108,11 +111,12 @@ jobs:
name: integration test / python ${{ matrix.python-version }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
python-version: ["3.7", "3.8", "3.9", "3.10"]
os: [ubuntu-latest]
include:
- python-version: 3.8
@@ -124,6 +128,9 @@ jobs:
TOXENV: integration
PYTEST_ADDOPTS: "-v --color=yes -n4 --csv integration_results.csv"
DBT_INVOCATION_ENV: github-actions
DBT_TEST_USER_1: dbt_test_user_1
DBT_TEST_USER_2: dbt_test_user_2
DBT_TEST_USER_3: dbt_test_user_3
steps:
- name: Check out the repository
@@ -148,9 +155,9 @@ jobs:
- name: Install python tools
run: |
pip install --user --upgrade pip
pip --version
pip install tox
python -m pip install --user --upgrade pip
python -m pip --version
python -m pip install tox
tox --version
- name: Run tests
@@ -189,9 +196,9 @@ jobs:
- name: Install python dependencies
run: |
pip install --user --upgrade pip
pip install --upgrade setuptools wheel twine check-wheel-contents
pip --version
python -m pip install --user --upgrade pip
python -m pip install --upgrade setuptools wheel twine check-wheel-contents
python -m pip --version
- name: Build distributions
run: ./scripts/build-dist.sh
@@ -209,7 +216,7 @@ jobs:
- name: Install wheel distributions
run: |
find ./dist/*.whl -maxdepth 1 -type f | xargs pip install --force-reinstall --find-links=dist/
find ./dist/*.whl -maxdepth 1 -type f | xargs python -m pip install --force-reinstall --find-links=dist/
- name: Check wheel distributions
run: |
@@ -218,7 +225,7 @@ jobs:
- name: Install source distributions
# ignore dbt-1.0.0, which intentionally raises an error when installed from source
run: |
find ./dist/dbt-[a-z]*.gz -maxdepth 1 -type f | xargs pip install --force-reinstall --find-links=dist/
find ./dist/dbt-[a-z]*.gz -maxdepth 1 -type f | xargs python -m pip install --force-reinstall --find-links=dist/
- name: Check source distributions
run: |

View File

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

View File

@@ -30,6 +30,11 @@ jobs:
LOG_DIR: "/home/runner/work/dbt-core/dbt-core/logs"
# tells integration tests to output into json format
DBT_LOG_FORMAT: "json"
# Additional test users
DBT_TEST_USER_1: dbt_test_user_1
DBT_TEST_USER_2: dbt_test_user_2
DBT_TEST_USER_3: dbt_test_user_3
steps:
- name: checkout dev
uses: actions/checkout@v2

View File

@@ -43,7 +43,7 @@ repos:
alias: flake8-check
stages: [manual]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.782
rev: v0.942
hooks:
- id: mypy
# N.B.: Mypy is... a bit fragile.

View File

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

View File

@@ -9,8 +9,7 @@ endif
.PHONY: dev
dev: ## Installs dbt-* packages in develop mode along with development dependencies.
@\
pip install -r dev-requirements.txt -r editable-requirements.txt && \
pre-commit install
pip install -r dev-requirements.txt -r editable-requirements.txt
.PHONY: mypy
mypy: .env ## Runs mypy against staged changes for static type checking.
@@ -34,27 +33,27 @@ lint: .env ## Runs flake8 and mypy code checks against staged changes.
$(DOCKER_CMD) pre-commit run mypy-check --hook-stage manual | grep -v "INFO"
.PHONY: unit
unit: .env ## Runs unit tests with py38.
unit: .env ## Runs unit tests with py
@\
$(DOCKER_CMD) tox -e py38
$(DOCKER_CMD) tox -e py
.PHONY: test
test: .env ## Runs unit tests with py38 and code checks against staged changes.
test: .env ## Runs unit tests with py and code checks against staged changes.
@\
$(DOCKER_CMD) tox -e py38; \
$(DOCKER_CMD) tox -e py; \
$(DOCKER_CMD) pre-commit run black-check --hook-stage manual | grep -v "INFO"; \
$(DOCKER_CMD) pre-commit run flake8-check --hook-stage manual | grep -v "INFO"; \
$(DOCKER_CMD) pre-commit run mypy-check --hook-stage manual | grep -v "INFO"
.PHONY: integration
integration: .env ## Runs postgres integration tests with py38.
integration: .env ## Runs postgres integration tests with py-integration
@\
$(DOCKER_CMD) tox -e py38-integration -- -nauto
$(DOCKER_CMD) tox -e py-integration -- -nauto
.PHONY: integration-fail-fast
integration-fail-fast: .env ## Runs postgres integration tests with py38 in "fail fast" mode.
integration-fail-fast: .env ## Runs postgres integration tests with py-integration in "fail fast" mode.
@\
$(DOCKER_CMD) tox -e py38-integration -- -x -nauto
$(DOCKER_CMD) tox -e py-integration -- -x -nauto
.PHONY: setup-db
setup-db: ## Setup Postgres database with docker-compose for system testing.

7
core/dbt/__init__.py Normal file
View File

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

View File

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

View File

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

View File

@@ -1,10 +1,24 @@
import abc
import os
from time import sleep
import sys
# multiprocessing.RLock is a function returning this type
from multiprocessing.synchronize import RLock
from threading import get_ident
from typing import Dict, Tuple, Hashable, Optional, ContextManager, List
from typing import (
Any,
Dict,
Tuple,
Hashable,
Optional,
ContextManager,
List,
Type,
Union,
Iterable,
Callable,
)
import agate
@@ -21,6 +35,7 @@ from dbt.contracts.graph.manifest import Manifest
from dbt.adapters.base.query_headers import (
MacroQueryStringSetter,
)
from dbt.events import AdapterLogger
from dbt.events.functions import fire_event
from dbt.events.types import (
NewConnection,
@@ -34,6 +49,9 @@ from dbt.events.types import (
)
from dbt import flags
SleepTime = Union[int, float] # As taken by time.sleep.
AdapterHandle = Any # Adapter connection handle objects can be any class.
class BaseConnectionManager(metaclass=abc.ABCMeta):
"""Methods to implement:
@@ -159,6 +177,94 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
conn.name = conn_name
return conn
@classmethod
def retry_connection(
cls,
connection: Connection,
connect: Callable[[], AdapterHandle],
logger: AdapterLogger,
retryable_exceptions: Iterable[Type[Exception]],
retry_limit: int = 1,
retry_timeout: Union[Callable[[int], SleepTime], SleepTime] = 1,
_attempts: int = 0,
) -> Connection:
"""Given a Connection, set its handle by calling connect.
The calls to connect will be retried up to retry_limit times to deal with transient
connection errors. By default, one retry will be attempted if retryable_exceptions is set.
:param Connection connection: An instance of a Connection that needs a handle to be set,
usually when attempting to open it.
:param connect: A callable that returns the appropiate connection handle for a
given adapter. This callable will be retried retry_limit times if a subclass of any
Exception in retryable_exceptions is raised by connect.
:type connect: Callable[[], AdapterHandle]
:param AdapterLogger logger: A logger to emit messages on retry attempts or errors. When
handling expected errors, we call debug, and call warning on unexpected errors or when
all retry attempts have been exhausted.
:param retryable_exceptions: An iterable of exception classes that if raised by
connect should trigger a retry.
:type retryable_exceptions: Iterable[Type[Exception]]
:param int retry_limit: How many times to retry the call to connect. If this limit
is exceeded before a successful call, a FailedToConnectException will be raised.
Must be non-negative.
:param retry_timeout: Time to wait between attempts to connect. Can also take a
Callable that takes the number of attempts so far, beginning at 0, and returns an int
or float to be passed to time.sleep.
:type retry_timeout: Union[Callable[[int], SleepTime], SleepTime] = 1
:param int _attempts: Parameter used to keep track of the number of attempts in calling the
connect function across recursive calls. Passed as an argument to retry_timeout if it
is a Callable. This parameter should not be set by the initial caller.
:raises dbt.exceptions.FailedToConnectException: Upon exhausting all retry attempts without
successfully acquiring a handle.
:return: The given connection with its appropriate state and handle attributes set
depending on whether we successfully acquired a handle or not.
"""
timeout = retry_timeout(_attempts) if callable(retry_timeout) else retry_timeout
if timeout < 0:
raise dbt.exceptions.FailedToConnectException(
"retry_timeout cannot be negative or return a negative time."
)
if retry_limit < 0 or retry_limit > sys.getrecursionlimit():
# This guard is not perfect others may add to the recursion limit (e.g. built-ins).
connection.handle = None
connection.state = ConnectionState.FAIL
raise dbt.exceptions.FailedToConnectException("retry_limit cannot be negative")
try:
connection.handle = connect()
connection.state = ConnectionState.OPEN
return connection
except tuple(retryable_exceptions) as e:
if retry_limit <= 0:
connection.handle = None
connection.state = ConnectionState.FAIL
raise dbt.exceptions.FailedToConnectException(str(e))
logger.debug(
f"Got a retryable error when attempting to open a {cls.TYPE} connection.\n"
f"{retry_limit} attempts remaining. Retrying in {timeout} seconds.\n"
f"Error:\n{e}"
)
sleep(timeout)
return cls.retry_connection(
connection=connection,
connect=connect,
logger=logger,
retry_limit=retry_limit - 1,
retry_timeout=retry_timeout,
retryable_exceptions=retryable_exceptions,
_attempts=_attempts + 1,
)
except Exception as e:
connection.handle = None
connection.state = ConnectionState.FAIL
raise dbt.exceptions.FailedToConnectException(str(e))
@abc.abstractmethod
def cancel_open(self) -> Optional[List[str]]:
"""Cancel all open connections on the adapter. (passable)"""
@@ -166,7 +272,8 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
"`cancel_open` is not implemented for this adapter!"
)
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def open(cls, connection: Connection) -> Connection:
"""Open the given connection on the adapter and return it.

View File

@@ -130,9 +130,15 @@ class BaseAdapter(metaclass=AdapterMeta):
methods are marked with a (passable) in their docstrings. Check docstrings
for type information, etc.
To implement a macro, implement "${adapter_type}__${macro_name}". in the
To implement a macro, implement "${adapter_type}__${macro_name}" in the
adapter's internal project.
To invoke a method in an adapter macro, call it on the 'adapter' Jinja
object using dot syntax.
To invoke a method in model code, add the @available decorator atop a method
declaration. Methods are invoked as macros.
Methods:
- exception_handler
- date_function
@@ -153,6 +159,7 @@ class BaseAdapter(metaclass=AdapterMeta):
- convert_datetime_type
- convert_date_type
- convert_time_type
- standardize_grants_dict
Macros:
- get_catalog
@@ -428,12 +435,14 @@ class BaseAdapter(metaclass=AdapterMeta):
###
# Abstract methods for database-specific values, attributes, and types
###
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def date_function(cls) -> str:
"""Get the date function used by this adapter's database."""
raise NotImplementedException("`date_function` is not implemented for this adapter!")
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def is_cancelable(cls) -> bool:
raise NotImplementedException("`is_cancelable` is not implemented for this adapter!")
@@ -530,6 +539,33 @@ class BaseAdapter(metaclass=AdapterMeta):
"`list_relations_without_caching` is not implemented for this " "adapter!"
)
###
# Methods about grants
###
@available
def standardize_grants_dict(self, grants_table: agate.Table) -> dict:
"""Translate the result of `show grants` (or equivalent) to match the
grants which a user would configure in their project.
Ideally, the SQL to show grants should also be filtering:
filter OUT any grants TO the current user/role (e.g. OWNERSHIP).
If that's not possible in SQL, it can be done in this method instead.
:param grants_table: An agate table containing the query result of
the SQL returned by get_show_grant_sql
:return: A standardized dictionary matching the `grants` config
:rtype: dict
"""
grants_dict: Dict[str, List[str]] = {}
for row in grants_table:
grantee = row["grantee"]
privilege = row["privilege_type"]
if privilege in grants_dict.keys():
grants_dict[privilege].append(grantee)
else:
grants_dict.update({privilege: [grantee]})
return grants_dict
###
# Provided methods about relations
###
@@ -728,7 +764,8 @@ class BaseAdapter(metaclass=AdapterMeta):
raise NotImplementedException("`drop_schema` is not implemented for this adapter!")
@available
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def quote(cls, identifier: str) -> str:
"""Quote the given identifier, as appropriate for the database."""
raise NotImplementedException("`quote` is not implemented for this adapter!")
@@ -774,7 +811,8 @@ class BaseAdapter(metaclass=AdapterMeta):
# Conversions: These must be implemented by concrete implementations, for
# converting agate types into their sql equivalents.
###
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def convert_text_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the agate.Text
type for the given agate table and column index.
@@ -785,7 +823,8 @@ class BaseAdapter(metaclass=AdapterMeta):
"""
raise NotImplementedException("`convert_text_type` is not implemented for this adapter!")
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def convert_number_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the agate.Number
type for the given agate table and column index.
@@ -796,7 +835,8 @@ class BaseAdapter(metaclass=AdapterMeta):
"""
raise NotImplementedException("`convert_number_type` is not implemented for this adapter!")
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def convert_boolean_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the agate.Boolean
type for the given agate table and column index.
@@ -809,7 +849,8 @@ class BaseAdapter(metaclass=AdapterMeta):
"`convert_boolean_type` is not implemented for this adapter!"
)
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def convert_datetime_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the agate.DateTime
type for the given agate table and column index.
@@ -822,7 +863,8 @@ class BaseAdapter(metaclass=AdapterMeta):
"`convert_datetime_type` is not implemented for this adapter!"
)
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def convert_date_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the agate.Date
type for the given agate table and column index.
@@ -833,7 +875,8 @@ class BaseAdapter(metaclass=AdapterMeta):
"""
raise NotImplementedException("`convert_date_type` is not implemented for this adapter!")
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def convert_time_type(cls, agate_table: agate.Table, col_idx: int) -> str:
"""Return the type in the database that best maps to the
agate.TimeDelta type for the given agate table and column index.

View File

@@ -140,8 +140,6 @@ class AdapterContainer:
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)

View File

@@ -7,7 +7,6 @@ from typing import (
List,
Generic,
TypeVar,
ClassVar,
Tuple,
Union,
Dict,
@@ -88,10 +87,13 @@ class AdapterProtocol( # type: ignore[misc]
Compiler_T,
],
):
AdapterSpecificConfigs: ClassVar[Type[AdapterConfig_T]]
Column: ClassVar[Type[Column_T]]
Relation: ClassVar[Type[Relation_T]]
ConnectionManager: ClassVar[Type[ConnectionManager_T]]
# N.B. Technically these are ClassVars, but mypy doesn't support putting type vars in a
# ClassVar due to the restirctiveness of PEP-526
# See: https://github.com/python/mypy/issues/5144
AdapterSpecificConfigs: Type[AdapterConfig_T]
Column: Type[Column_T]
Relation: Type[Relation_T]
ConnectionManager: Type[ConnectionManager_T]
connections: ConnectionManager_T
def __init__(self, config: AdapterRequiredConfig):

View File

@@ -1,7 +1,7 @@
# this module exists to resolve circular imports with the events module
from collections import namedtuple
from typing import Optional
from typing import Any, Optional
_ReferenceKey = namedtuple("_ReferenceKey", "database schema identifier")
@@ -14,7 +14,7 @@ def lowercase(value: Optional[str]) -> Optional[str]:
return value.lower()
def _make_key(relation) -> _ReferenceKey:
def _make_key(relation: Any) -> _ReferenceKey:
"""Make _ReferenceKeys with lowercase values for the cache so we don't have
to keep track of quoting
"""

View File

@@ -77,7 +77,8 @@ class SQLConnectionManager(BaseConnectionManager):
return connection, cursor
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def get_response(cls, cursor: Any) -> AdapterResponse:
"""Get the status of the cursor."""
raise dbt.exceptions.NotImplementedException(

View File

@@ -246,16 +246,17 @@ def _supports_long_paths() -> bool:
# 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()
else:
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:
@@ -443,7 +444,11 @@ def download_with_retries(
connection_exception_retry(download_fn, 5)
def download(url: str, path: str, timeout: Optional[Union[float, tuple]] = None) -> None:
def download(
url: str,
path: str,
timeout: Optional[Union[float, Tuple[float, float], Tuple[float, None]]] = None,
) -> None:
path = convert_path(path)
connection_timeout = timeout or float(os.getenv("DBT_HTTP_TIMEOUT", 10))
response = requests.get(url, timeout=connection_timeout)

View File

@@ -8,7 +8,6 @@ try:
except ImportError:
from yaml import Loader, SafeLoader, Dumper # type: ignore # noqa: F401
from dbt.ui import warning_tag
YAML_ERROR_MESSAGE = """
Syntax error near line {line_number}
@@ -21,26 +20,6 @@ Raw Error:
""".strip()
class UniqueKeyLoader(SafeLoader):
"""A subclass that checks for unique yaml mapping nodes.
This class extends `SafeLoader` from the `yaml` library to check for
unique top level keys (mapping nodes). See issue (https://github.com/yaml/pyyaml/issues/165)
and solution (https://gist.github.com/pypt/94d747fe5180851196eb?permalink_comment_id=4015118).
"""
def construct_mapping(self, node, deep=False):
mapping = set()
for key_node, value_node in node.value:
key = self.construct_object(key_node, deep=deep)
if key in mapping:
raise dbt.exceptions.DuplicateYamlKeyException(
f"Duplicate {key!r} key found in yaml file"
)
mapping.add(key)
return super().construct_mapping(node, deep)
def line_no(i, line, width=3):
line_number = str(i).ljust(width)
return "{}| {}".format(line_number, line)
@@ -69,7 +48,7 @@ def contextualized_yaml_error(raw_contents, error):
def safe_load(contents) -> Optional[Dict[str, Any]]:
return yaml.load(contents, Loader=UniqueKeyLoader)
return yaml.load(contents, Loader=SafeLoader)
def load_yaml_text(contents, path=None):
@@ -82,7 +61,3 @@ def load_yaml_text(contents, path=None):
error = str(e)
raise dbt.exceptions.ValidationException(error)
except dbt.exceptions.DuplicateYamlKeyException as e:
# TODO: We may want to raise an exception instead of a warning in the future.
e.msg = f"{e} {path.searched_path}/{path.relative_path}."
dbt.exceptions.warn_or_raise(e, log_fmt=warning_tag("{}"))

View File

@@ -397,6 +397,8 @@ class Compiler:
linker.dependency(node.unique_id, (manifest.nodes[dependency].unique_id))
elif dependency in manifest.sources:
linker.dependency(node.unique_id, (manifest.sources[dependency].unique_id))
elif dependency in manifest.metrics:
linker.dependency(node.unique_id, (manifest.metrics[dependency].unique_id))
else:
dependency_not_found(node, dependency)

View File

@@ -15,7 +15,7 @@ from typing_extensions import Protocol, runtime_checkable
import hashlib
import os
from dbt import deprecations
from dbt import flags, deprecations
from dbt.clients.system import resolve_path_from_base
from dbt.clients.system import path_exists
from dbt.clients.system import load_file_contents
@@ -142,6 +142,13 @@ def _all_source_paths(
T = TypeVar("T")
def flag_or(flag: Optional[T], value: Optional[T], default: T) -> T:
if flag is None:
return value_or(value, default)
else:
return flag
def value_or(value: Optional[T], default: T) -> T:
if value is None:
return default
@@ -356,9 +363,9 @@ class PartialProject(RenderComponents):
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")
target_path: str = flag_or(flags.TARGET_PATH, cfg.target_path, "target")
clean_targets: List[str] = value_or(cfg.clean_targets, [target_path])
log_path: str = value_or(cfg.log_path, "logs")
log_path: str = flag_or(flags.LOG_PATH, cfg.log_path, "logs")
packages_install_path: str = value_or(cfg.packages_install_path, "dbt_packages")
# in the default case we'll populate this once we know the adapter type
# It would be nice to just pass along a Quoting here, but that would

View File

@@ -1,12 +1,15 @@
from typing import Dict, Any, Tuple, Optional, Union, Callable
import re
import os
from dbt.clients.jinja import get_rendered, catch_jinja
from dbt.context.target import TargetContext
from dbt.context.secret import SecretContext
from dbt.context.secret import SecretContext, SECRET_PLACEHOLDER
from dbt.context.base import BaseContext
from dbt.contracts.connection import HasCredentials
from dbt.exceptions import DbtProjectError, CompilationException, RecursionException
from dbt.utils import deep_map_render
from dbt.logger import SECRET_ENV_PREFIX
Keypath = Tuple[Union[str, int], ...]
@@ -174,6 +177,28 @@ class SecretRenderer(BaseRenderer):
def name(self):
return "Secret"
def render_value(self, value: Any, keypath: Optional[Keypath] = None) -> Any:
# First, standard Jinja rendering, with special handling for 'secret' environment variables
# "{{ env_var('DBT_SECRET_ENV_VAR') }}" -> "$$$DBT_SECRET_START$$$DBT_SECRET_ENV_{VARIABLE_NAME}$$$DBT_SECRET_END$$$"
# This prevents Jinja manipulation of secrets via macros/filters that might leak partial/modified values in logs
rendered = super().render_value(value, keypath)
# Now, detect instances of the placeholder value ($$$DBT_SECRET_START...DBT_SECRET_END$$$)
# and replace them with the actual secret value
if SECRET_ENV_PREFIX in str(rendered):
search_group = f"({SECRET_ENV_PREFIX}(.*))"
pattern = SECRET_PLACEHOLDER.format(search_group).replace("$", r"\$")
m = re.search(
pattern,
rendered,
)
if m:
found = m.group(1)
value = os.environ[found]
replace_this = SECRET_PLACEHOLDER.format(found)
return rendered.replace(replace_this, value)
else:
return rendered
class ProfileRenderer(SecretRenderer):
@property

View File

@@ -1,7 +1,7 @@
import itertools
import os
from copy import deepcopy
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Any, Optional, Mapping, Iterator, Iterable, Tuple, List, MutableSet, Type
@@ -417,6 +417,9 @@ class UnsetProfileConfig(RuntimeConfig):
missing, any access to profile members results in an exception.
"""
profile_name: str = field(repr=False)
target_name: str = field(repr=False)
def __post_init__(self):
# instead of futzing with InitVar overrides or rewriting __init__, just
# `del` the attrs we don't want users touching.
@@ -437,6 +440,56 @@ class UnsetProfileConfig(RuntimeConfig):
# re-override the poisoned profile behavior
return DictDefaultEmptyStr({})
def to_project_config(self, with_packages=False):
"""Return a dict representation of the config that could be written to
disk with `yaml.safe_dump` to get this configuration.
Overrides dbt.config.Project.to_project_config to omit undefined profile
attributes.
:param with_packages bool: If True, include the serialized packages
file in the root.
:returns dict: The serialized profile.
"""
result = deepcopy(
{
"name": self.project_name,
"version": self.version,
"project-root": self.project_root,
"profile": "",
"model-paths": self.model_paths,
"macro-paths": self.macro_paths,
"seed-paths": self.seed_paths,
"test-paths": self.test_paths,
"analysis-paths": self.analysis_paths,
"docs-paths": self.docs_paths,
"asset-paths": self.asset_paths,
"target-path": self.target_path,
"snapshot-paths": self.snapshot_paths,
"clean-targets": self.clean_targets,
"log-path": self.log_path,
"quoting": self.quoting,
"models": self.models,
"on-run-start": self.on_run_start,
"on-run-end": self.on_run_end,
"dispatch": self.dispatch,
"seeds": self.seeds,
"snapshots": self.snapshots,
"sources": self.sources,
"tests": self.tests,
"vars": self.vars.to_dict(),
"require-dbt-version": [v.to_version_string() for v in self.dbt_version],
"config-version": self.config_version,
}
)
if self.query_comment:
result["query-comment"] = self.query_comment.to_dict(omit_none=True)
if with_packages:
result.update(self.packages.to_dict(omit_none=True))
return result
@classmethod
def from_parts(
cls,

View File

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

View File

@@ -1,6 +1,6 @@
import json
import os
from typing import Any, Dict, NoReturn, Optional, Mapping
from typing import Any, Dict, NoReturn, Optional, Mapping, Iterable, Set, List
from dbt import flags
from dbt import tracking
@@ -8,8 +8,9 @@ from dbt.clients.jinja import get_rendered
from dbt.clients.yaml_helper import yaml, safe_load, SafeLoader, Loader, Dumper # noqa: F401
from dbt.contracts.graph.compiled import CompiledResource
from dbt.exceptions import (
raise_compiler_error,
CompilationException,
MacroReturn,
raise_compiler_error,
raise_parsing_error,
disallow_secret_env_var,
)
@@ -25,38 +26,7 @@ import datetime
import re
import itertools
# Contexts in dbt Core
# Contexts are used for Jinja rendering. They include context methods,
# executable macros, and various settings that are available in Jinja.
#
# Different contexts are used in different places because we allow access
# to different methods and data in different places. Executable SQL, for
# example, includes the available macros and the model, while Jinja in
# yaml files is more limited.
#
# The context that is passed to Jinja is always in a dictionary format,
# not an actual class, so a 'to_dict()' is executed on a context class
# before it is used for rendering.
#
# Each context has a generate_<name>_context function to create the context.
# ProviderContext subclasses have different generate functions for
# parsing and for execution.
#
# Context class hierarchy
#
# BaseContext -- core/dbt/context/base.py
# SecretContext -- core/dbt/context/secret.py
# TargetContext -- core/dbt/context/target.py
# ConfiguredContext -- core/dbt/context/configured.py
# SchemaYamlContext -- core/dbt/context/configured.py
# DocsRuntimeContext -- core/dbt/context/configured.py
# MacroResolvingContext -- core/dbt/context/configured.py
# ManifestContext -- core/dbt/context/manifest.py
# QueryHeaderContext -- core/dbt/context/manifest.py
# ProviderContext -- core/dbt/context/provider.py
# MacroContext -- core/dbt/context/provider.py
# ModelContext -- core/dbt/context/provider.py
# TestContext -- core/dbt/context/provider.py
# See the `contexts` module README for more information on how contexts work
def get_pytz_module_context() -> Dict[str, Any]:
@@ -482,6 +452,90 @@ class BaseContext(metaclass=ContextMeta):
except (ValueError, yaml.YAMLError):
return default
@contextmember("set")
@staticmethod
def _set(value: Iterable[Any], default: Any = None) -> Optional[Set[Any]]:
"""The `set` context method can be used to convert any iterable
to a sequence of iterable elements that are unique (a set).
:param value: The iterable
:param default: A default value to return if the `value` argument
is not an iterable
Usage:
{% set my_list = [1, 2, 2, 3] %}
{% set my_set = set(my_list) %}
{% do log(my_set) %} {# {1, 2, 3} #}
"""
try:
return set(value)
except TypeError:
return default
@contextmember
@staticmethod
def set_strict(value: Iterable[Any]) -> Set[Any]:
"""The `set_strict` context method can be used to convert any iterable
to a sequence of iterable elements that are unique (a set). The
difference to the `set` context method is that the `set_strict` method
will raise an exception on a TypeError.
:param value: The iterable
Usage:
{% set my_list = [1, 2, 2, 3] %}
{% set my_set = set_strict(my_list) %}
{% do log(my_set) %} {# {1, 2, 3} #}
"""
try:
return set(value)
except TypeError as e:
raise CompilationException(e)
@contextmember("zip")
@staticmethod
def _zip(*args: Iterable[Any], default: Any = None) -> Optional[Iterable[Any]]:
"""The `zip` context method can be used to used to return
an iterator of tuples, where the i-th tuple contains the i-th
element from each of the argument iterables.
:param *args: Any number of iterables
:param default: A default value to return if `*args` is not
iterable
Usage:
{% set my_list_a = [1, 2] %}
{% set my_list_b = ['alice', 'bob'] %}
{% set my_zip = zip(my_list_a, my_list_b) | list %}
{% do log(my_set) %} {# [(1, 'alice'), (2, 'bob')] #}
"""
try:
return zip(*args)
except TypeError:
return default
@contextmember
@staticmethod
def zip_strict(*args: Iterable[Any]) -> Iterable[Any]:
"""The `zip_strict` context method can be used to used to return
an iterator of tuples, where the i-th tuple contains the i-th
element from each of the argument iterables. The difference to the
`zip` context method is that the `zip_strict` method will raise an
exception on a TypeError.
:param *args: Any number of iterables
Usage:
{% set my_list_a = [1, 2] %}
{% set my_list_b = ['alice', 'bob'] %}
{% set my_zip = zip_strict(my_list_a, my_list_b) | list %}
{% do log(my_set) %} {# [(1, 'alice'), (2, 'bob')] #}
"""
try:
return zip(*args)
except TypeError as e:
raise CompilationException(e)
@contextmember
@staticmethod
def log(msg: str, info: bool = False) -> str:
@@ -599,6 +653,35 @@ class BaseContext(metaclass=ContextMeta):
print(msg)
return ""
@contextmember
@staticmethod
def diff_of_two_dicts(
dict_a: Dict[str, List[str]], dict_b: Dict[str, List[str]]
) -> Dict[str, List[str]]:
"""
Given two dictionaries of type Dict[str, List[str]]:
dict_a = {'key_x': ['value_1', 'VALUE_2'], 'KEY_Y': ['value_3']}
dict_b = {'key_x': ['value_1'], 'key_z': ['value_4']}
Return the same dictionary representation of dict_a MINUS dict_b,
performing a case-insensitive comparison between the strings in each.
All keys returned will be in the original case of dict_a.
returns {'key_x': ['VALUE_2'], 'KEY_Y': ['value_3']}
"""
dict_diff = {}
dict_b_lowered = {k.casefold(): [x.casefold() for x in v] for k, v in dict_b.items()}
for k in dict_a:
if k.casefold() in dict_b_lowered.keys():
diff = []
for v in dict_a[k]:
if v.casefold() not in dict_b_lowered[k.casefold()]:
diff.append(v)
if diff:
dict_diff.update({k: diff})
else:
dict_diff.update({k: dict_a[k]})
return dict_diff
def generate_base_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]:
ctx = BaseContext(cli_vars)

View File

@@ -4,7 +4,7 @@ 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.contracts.graph.model_config import BaseConfig, get_config_for, _listify
from dbt.exceptions import InternalException
from dbt.node_types import NodeType
from dbt.utils import fqn_search
@@ -264,18 +264,49 @@ class ContextConfig:
@classmethod
def _add_config_call(cls, config_call_dict, opts: Dict[str, Any]) -> None:
# config_call_dict is already encountered configs, opts is new
# This mirrors code in _merge_field_value in model_config.py which is similar but
# operates on config objects.
for k, v in opts.items():
# 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)
if k in config_call_dict: # should always be a list here
config_call_dict[k].extend(v)
else:
config_call_dict[k] = v
elif k in BaseConfig.mergebehavior["update"]:
if not isinstance(v, dict):
raise InternalException(f"expected dict, got {v}")
if k in config_call_dict and isinstance(config_call_dict[k], dict):
config_call_dict[k].update(v)
else:
config_call_dict[k] = v
elif k in BaseConfig.mergebehavior["dict_key_append"]:
if not isinstance(v, dict):
raise InternalException(f"expected dict, got {v}")
if k in config_call_dict: # should always be a dict
for key, value in v.items():
extend = False
# This might start with a +, to indicate we should extend the list
# instead of just clobbering it
if key.startswith("+"):
extend = True
if key in config_call_dict[k] and extend:
# extend the list
config_call_dict[k][key].extend(_listify(value))
else:
# clobber the list
config_call_dict[k][key] = _listify(value)
else:
# This is always a dictionary
config_call_dict[k] = v
# listify everything
for key, value in config_call_dict[k].items():
config_call_dict[k][key] = _listify(value)
else:
config_call_dict[k] = v

View File

@@ -40,6 +40,7 @@ from dbt.contracts.graph.parsed import (
ParsedSeedNode,
ParsedSourceDefinition,
)
from dbt.contracts.graph.metrics import MetricReference, ResolvedMetricReference
from dbt.exceptions import (
CompilationException,
ParsingException,
@@ -50,7 +51,9 @@ from dbt.exceptions import (
missing_config,
raise_compiler_error,
ref_invalid_args,
metric_invalid_args,
ref_target_not_found,
metric_target_not_found,
ref_bad_context,
source_target_not_found,
wrapped_exports,
@@ -199,7 +202,7 @@ class BaseResolver(metaclass=abc.ABCMeta):
return self.db_wrapper.Relation
@abc.abstractmethod
def __call__(self, *args: str) -> Union[str, RelationProxy]:
def __call__(self, *args: str) -> Union[str, RelationProxy, MetricReference]:
pass
@@ -265,6 +268,41 @@ class BaseSourceResolver(BaseResolver):
return self.resolve(args[0], args[1])
class BaseMetricResolver(BaseResolver):
def resolve(self, name: str, package: Optional[str] = None) -> MetricReference:
...
def _repack_args(self, name: str, package: Optional[str]) -> List[str]:
if package is None:
return [name]
else:
return [package, name]
def validate_args(self, name: str, package: Optional[str]):
if not isinstance(name, str):
raise CompilationException(
f"The name argument to metric() must be a string, got {type(name)}"
)
if package is not None and not isinstance(package, str):
raise CompilationException(
f"The package argument to metric() must be a string or None, got {type(package)}"
)
def __call__(self, *args: str) -> MetricReference:
name: str
package: Optional[str] = None
if len(args) == 1:
name = args[0]
elif len(args) == 2:
package, name = args
else:
metric_invalid_args(self.model, args)
self.validate_args(name, package)
return self.resolve(name, package)
class Config(Protocol):
def __init__(self, model, context_config: Optional[ContextConfig]):
...
@@ -511,6 +549,34 @@ class RuntimeSourceResolver(BaseSourceResolver):
return self.Relation.create_from_source(target_source)
# metric` implementations
class ParseMetricResolver(BaseMetricResolver):
def resolve(self, name: str, package: Optional[str] = None) -> MetricReference:
self.model.metrics.append(self._repack_args(name, package))
return MetricReference(name, package)
class RuntimeMetricResolver(BaseMetricResolver):
def resolve(self, target_name: str, target_package: Optional[str] = None) -> MetricReference:
target_metric = self.manifest.resolve_metric(
target_name,
target_package,
self.current_project,
self.model.package_name,
)
if target_metric is None or isinstance(target_metric, Disabled):
# TODO : Use a different exception!!
metric_target_not_found(
self.model,
target_name,
target_package,
)
return ResolvedMetricReference(target_metric, self.manifest, self.Relation)
# `var` implementations.
class ModelConfiguredVar(Var):
def __init__(
@@ -568,6 +634,7 @@ class Provider(Protocol):
Var: Type[ModelConfiguredVar]
ref: Type[BaseRefResolver]
source: Type[BaseSourceResolver]
metric: Type[BaseMetricResolver]
class ParseProvider(Provider):
@@ -577,6 +644,7 @@ class ParseProvider(Provider):
Var = ParseVar
ref = ParseRefResolver
source = ParseSourceResolver
metric = ParseMetricResolver
class GenerateNameProvider(Provider):
@@ -586,6 +654,7 @@ class GenerateNameProvider(Provider):
Var = RuntimeVar
ref = ParseRefResolver
source = ParseSourceResolver
metric = ParseMetricResolver
class RuntimeProvider(Provider):
@@ -595,6 +664,7 @@ class RuntimeProvider(Provider):
Var = RuntimeVar
ref = RuntimeRefResolver
source = RuntimeSourceResolver
metric = RuntimeMetricResolver
class OperationProvider(RuntimeProvider):
@@ -778,6 +848,10 @@ class ProviderContext(ManifestContext):
def source(self) -> Callable:
return self.provider.source(self.db_wrapper, self.model, self.config, self.manifest)
@contextproperty
def metric(self) -> Callable:
return self.provider.metric(self.db_wrapper, self.model, self.config, self.manifest)
@contextproperty("config")
def ctx_config(self) -> Config:
"""The `config` variable exists to handle end-user configuration for
@@ -1355,7 +1429,7 @@ class MetricRefResolver(BaseResolver):
if not isinstance(name, str):
raise ParsingException(
f"In a metrics section in {self.model.original_file_path} "
f"the name argument to ref() must be a string"
"the name argument to ref() must be a string"
)
@@ -1373,6 +1447,12 @@ def generate_parse_metrics(
project,
manifest,
),
"metric": ParseMetricResolver(
None,
metric,
project,
manifest,
),
}

View File

@@ -7,6 +7,9 @@ from dbt.exceptions import raise_parsing_error
from dbt.logger import SECRET_ENV_PREFIX
SECRET_PLACEHOLDER = "$$$DBT_SECRET_START$$${}$$$DBT_SECRET_END$$$"
class SecretContext(BaseContext):
"""This context is used in profiles.yml + packages.yml. It can render secret
env vars that aren't usable elsewhere"""
@@ -18,21 +21,29 @@ class SecretContext(BaseContext):
If the default is None, raise an exception for an undefined variable.
In this context *only*, env_var will return the actual values of
env vars prefixed with DBT_ENV_SECRET_
In this context *only*, env_var will accept env vars prefixed with DBT_ENV_SECRET_.
It will return the name of the secret env var, wrapped in 'start' and 'end' identifiers.
The actual value will be subbed in later in SecretRenderer.render_value()
"""
return_value = None
if var in os.environ:
# if this is a 'secret' env var, just return the name of the env var
# instead of rendering the actual value here, to avoid any risk of
# Jinja manipulation. it will be subbed out later, in SecretRenderer.render_value
if var in os.environ and var.startswith(SECRET_ENV_PREFIX):
return SECRET_PLACEHOLDER.format(var)
elif var in os.environ:
return_value = os.environ[var]
elif default is not None:
return_value = default
if return_value is not None:
# do not save secret environment variables
# store env vars in the internal manifest to power partial parsing
# if it's a 'secret' env var, we shouldn't even get here
# but just to be safe — don't save secrets
if not var.startswith(SECRET_ENV_PREFIX):
self.env_vars[var] = return_value
# return the value even if its a secret
return return_value
else:
msg = f"Env var required but not provided: '{var}'"

View File

@@ -183,6 +183,39 @@ class RefableLookup(dbtClassMixin):
return manifest.nodes[unique_id]
class MetricLookup(dbtClassMixin):
def __init__(self, manifest: "Manifest"):
self.storage: Dict[str, Dict[PackageName, UniqueID]] = {}
self.populate(manifest)
def get_unique_id(self, search_name, package: Optional[PackageName]):
return find_unique_id_for_package(self.storage, search_name, package)
def find(self, search_name, package: Optional[PackageName], manifest: "Manifest"):
unique_id = self.get_unique_id(search_name, package)
if unique_id is not None:
return self.perform_lookup(unique_id, manifest)
return None
def add_metric(self, metric: ParsedMetric):
if metric.search_name not in self.storage:
self.storage[metric.search_name] = {}
self.storage[metric.search_name][metric.package_name] = metric.unique_id
def populate(self, manifest):
for metric in manifest.metrics.values():
if hasattr(metric, "name"):
self.add_metric(metric)
def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> ParsedMetric:
if unique_id not in manifest.metrics:
raise dbt.exceptions.InternalException(
f"Metric {unique_id} found in cache but not found in manifest"
)
return manifest.metrics[unique_id]
# This handles both models/seeds/snapshots and sources
class DisabledLookup(dbtClassMixin):
def __init__(self, manifest: "Manifest"):
@@ -328,11 +361,6 @@ class Locality(enum.IntEnum):
Root = 3
class Specificity(enum.IntEnum):
Default = 1
Adapter = 2
@dataclass
class MacroCandidate:
locality: Locality
@@ -355,12 +383,14 @@ class MacroCandidate:
@dataclass
class MaterializationCandidate(MacroCandidate):
specificity: Specificity
# specificity describes where in the inheritance chain this materialization candidate is
# a specificity of 0 means a materialization defined by the current adapter
# the highest the specificity describes a default materialization. the value itself depends on
# how many adapters there are in the inheritance chain
specificity: int
@classmethod
def from_macro(
cls, candidate: MacroCandidate, specificity: Specificity
) -> "MaterializationCandidate":
def from_macro(cls, candidate: MacroCandidate, specificity: int) -> "MaterializationCandidate":
return cls(
locality=candidate.locality,
macro=candidate.macro,
@@ -384,9 +414,9 @@ class MaterializationCandidate(MacroCandidate):
def __lt__(self, other: object) -> bool:
if not isinstance(other, MaterializationCandidate):
return NotImplemented
if self.specificity < other.specificity:
return True
if self.specificity > other.specificity:
return True
if self.specificity < other.specificity:
return False
if self.locality < other.locality:
return True
@@ -434,6 +464,9 @@ class Disabled(Generic[D]):
target: D
MaybeMetricNode = Optional[ParsedMetric]
MaybeDocumentation = Optional[ParsedDocumentation]
@@ -595,6 +628,9 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
_ref_lookup: Optional[RefableLookup] = field(
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
)
_metric_lookup: Optional[MetricLookup] = field(
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
)
_disabled_lookup: Optional[DisabledLookup] = field(
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
)
@@ -671,18 +707,24 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
disabled_by_file_id[node.file_id] = node
return disabled_by_file_id
def _get_parent_adapter_types(self, adapter_type: str) -> List[str]:
# This is duplicated logic from core/dbt/context/providers.py
# Ideally this would instead be incorporating actual dispatch logic
from dbt.adapters.factory import get_adapter_type_names
# order matters for dispatch:
# 1. current adapter
# 2. any parent adapters (dependencies)
# 3. 'default'
return get_adapter_type_names(adapter_type) + ["default"]
def _materialization_candidates_for(
self,
project_name: str,
materialization_name: str,
adapter_type: Optional[str],
adapter_type: str,
specificity: int,
) -> CandidateList:
if adapter_type is None:
specificity = Specificity.Default
else:
specificity = Specificity.Adapter
full_name = dbt.utils.get_materialization_macro_name(
materialization_name=materialization_name,
adapter_type=adapter_type,
@@ -702,8 +744,9 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
project_name=project_name,
materialization_name=materialization_name,
adapter_type=atype,
specificity=specificity, # where in the inheritance chain this candidate is
)
for atype in (adapter_type, None)
for specificity, atype in enumerate(self._get_parent_adapter_types(adapter_type))
)
)
return candidates.last()
@@ -833,6 +876,12 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self._ref_lookup = RefableLookup(self)
return self._ref_lookup
@property
def metric_lookup(self) -> MetricLookup:
if self._metric_lookup is None:
self._metric_lookup = MetricLookup(self)
return self._metric_lookup
def rebuild_ref_lookup(self):
self._ref_lookup = RefableLookup(self)
@@ -908,6 +957,22 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
return Disabled(disabled[0])
return None
def resolve_metric(
self,
target_metric_name: str,
target_metric_package: Optional[str],
current_project: str,
node_package: str,
) -> MaybeMetricNode:
metric: Optional[ParsedMetric] = None
candidates = _search_packages(current_project, node_package, target_metric_package)
for pkg in candidates:
metric = self.metric_lookup.find(target_metric_name, pkg, self)
if metric is not None:
return metric
return None
# Called by DocsRuntimeContext.doc
def resolve_doc(
self,
@@ -1072,6 +1137,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self._doc_lookup,
self._source_lookup,
self._ref_lookup,
self._metric_lookup,
self._disabled_lookup,
self._analysis_lookup,
)
@@ -1091,7 +1157,7 @@ AnyManifest = Union[Manifest, MacroManifest]
@dataclass
@schema_version("manifest", 5)
@schema_version("manifest", 6)
class WritableManifest(ArtifactMixin):
nodes: Mapping[UniqueID, ManifestNode] = field(
metadata=dict(description=("The nodes defined in the dbt project and its dependencies"))
@@ -1135,6 +1201,16 @@ class WritableManifest(ArtifactMixin):
)
)
@classmethod
def compatible_previous_versions(self):
return [("manifest", 4), ("manifest", 5)]
def __post_serialize__(self, dct):
for unique_id, node in dct["nodes"].items():
if "config_call_dict" in node:
del node["config_call_dict"]
return dct
def _check_duplicates(value: HasUniqueID, src: Mapping[str, HasUniqueID]):
if value.unique_id in src:

View File

@@ -0,0 +1,70 @@
from dbt.node_types import NodeType
class MetricReference(object):
def __init__(self, metric_name, package_name=None):
self.metric_name = metric_name
self.package_name = package_name
def __str__(self):
return f"{self.metric_name}"
class ResolvedMetricReference(MetricReference):
"""
Simple proxy over a ParsedMetric which delegates property
lookups to the underlying node. Also adds helper functions
for working with metrics (ie. __str__ and templating functions)
"""
def __init__(self, node, manifest, Relation):
super().__init__(node.name, node.package_name)
self.node = node
self.manifest = manifest
self.Relation = Relation
def __getattr__(self, key):
return getattr(self.node, key)
def __str__(self):
return f"{self.node.name}"
@classmethod
def parent_metrics(cls, metric_node, manifest):
yield metric_node
for parent_unique_id in metric_node.depends_on.nodes:
node = manifest.metrics.get(parent_unique_id)
if node and node.resource_type == NodeType.Metric:
yield from cls.parent_metrics(node, manifest)
def parent_models(self):
in_scope_metrics = list(self.parent_metrics(self.node, self.manifest))
to_return = {
"base": [],
"derived": [],
}
for metric in in_scope_metrics:
if metric.type == "expression":
to_return["derived"].append(
{"metric_source": None, "metric": metric, "is_derived": True}
)
else:
for node_unique_id in metric.depends_on.nodes:
node = self.manifest.nodes.get(node_unique_id)
if node and node.resource_type in NodeType.refable():
to_return["base"].append(
{
"metric_relation_node": node,
"metric_relation": self.Relation.create(
database=node.database,
schema=node.schema,
identifier=node.alias,
),
"metric": metric,
"is_derived": False,
}
)
return to_return

View File

@@ -7,7 +7,8 @@ from dbt.dataclass_schema import (
ValidationError,
register_pattern,
)
from dbt.contracts.graph.unparsed import AdditionalPropertiesAllowed
from dbt.contracts.graph.unparsed import AdditionalPropertiesAllowed, Docs
from dbt.contracts.graph.utils import validate_color
from dbt.exceptions import InternalException, CompilationException
from dbt.contracts.util import Replaceable, list_str
from dbt import hooks
@@ -66,6 +67,7 @@ class MergeBehavior(Metadata):
Append = 1
Update = 2
Clobber = 3
DictKeyAppend = 4
@classmethod
def default_field(cls) -> "MergeBehavior":
@@ -124,6 +126,9 @@ def _listify(value: Any) -> List:
return [value]
# There are two versions of this code. The one here is for config
# objects, the one in _add_config_call in context_config.py is for
# config_call_dict dictionaries.
def _merge_field_value(
merge_behavior: MergeBehavior,
self_value: Any,
@@ -141,6 +146,31 @@ def _merge_field_value(
value = self_value.copy()
value.update(other_value)
return value
elif merge_behavior == MergeBehavior.DictKeyAppend:
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}")
new_dict = {}
for key in self_value.keys():
new_dict[key] = _listify(self_value[key])
for key in other_value.keys():
extend = False
new_key = key
# This might start with a +, to indicate we should extend the list
# instead of just clobbering it
if new_key.startswith("+"):
new_key = key.lstrip("+")
extend = True
if new_key in new_dict and extend:
# extend the list
value = other_value[key]
new_dict[new_key].extend(_listify(value))
else:
# clobber the list
new_dict[new_key] = _listify(other_value[key])
return new_dict
else:
raise InternalException(f"Got an invalid merge_behavior: {merge_behavior}")
@@ -256,7 +286,8 @@ class BaseConfig(AdditionalPropertiesAllowed, Replaceable):
# 'meta' moved here from node
mergebehavior = {
"append": ["pre-hook", "pre_hook", "post-hook", "post_hook", "tags"],
"update": ["quoting", "column_types", "meta"],
"update": ["quoting", "column_types", "meta", "docs"],
"dict_key_append": ["grants"],
}
@classmethod
@@ -427,6 +458,23 @@ class NodeConfig(NodeAndTestConfig):
# sometimes getting the Union order wrong, causing serialization failures.
unique_key: Union[str, List[str], None] = None
on_schema_change: Optional[str] = "ignore"
grants: Dict[str, Any] = field(
default_factory=dict, metadata=MergeBehavior.DictKeyAppend.meta()
)
docs: Docs = field(
default_factory=lambda: Docs(show=True),
metadata=MergeBehavior.Update.meta(),
)
# we validate that node_color has a suitable value to prevent dbt-docs from crashing
def __post_init__(self):
if self.docs.node_color:
node_color = self.docs.node_color
if not validate_color(node_color):
raise ValidationError(
f"Invalid color name for docs.node_color: {node_color}. "
"It is neither a valid HTML color name nor a valid HEX code."
)
@classmethod
def __pre_deserialize__(cls, data):

View File

@@ -157,7 +157,6 @@ class ParsedNodeMixins(dbtClassMixin):
self.created_at = time.time()
self.description = patch.description
self.columns = patch.columns
self.docs = patch.docs
def get_materialization(self):
return self.config.materialized
@@ -198,11 +197,12 @@ class ParsedNodeDefaults(NodeInfoMixin, ParsedNodeMandatory):
tags: List[str] = field(default_factory=list)
refs: List[List[str]] = field(default_factory=list)
sources: List[List[str]] = field(default_factory=list)
metrics: List[List[str]] = field(default_factory=list)
depends_on: DependsOn = field(default_factory=DependsOn)
description: str = field(default="")
columns: Dict[str, ColumnInfo] = field(default_factory=dict)
meta: Dict[str, Any] = field(default_factory=dict)
docs: Docs = field(default_factory=Docs)
docs: Docs = field(default_factory=lambda: Docs(show=True))
patch_path: Optional[str] = None
compiled_path: Optional[str] = None
build_path: Optional[str] = None
@@ -233,8 +233,6 @@ class ParsedNode(ParsedNodeDefaults, ParsedNodeMixins, SerializableType):
return self.to_dict()
def __post_serialize__(self, dct):
if "config_call_dict" in dct:
del dct["config_call_dict"]
if "_event_status" in dct:
del dct["_event_status"]
return dct
@@ -586,10 +584,7 @@ class UnpatchedSourceDefinition(UnparsedBaseNode, HasUniqueID, HasFqn):
@property
def columns(self) -> Sequence[UnparsedColumn]:
if self.table.columns is None:
return []
else:
return self.table.columns
return [] if self.table.columns is None else self.table.columns
def get_tests(self) -> Iterator[Tuple[Dict[str, Any], Optional[UnparsedColumn]]]:
for test in self.tests:
@@ -798,24 +793,32 @@ class ParsedExposure(UnparsedBaseNode, HasUniqueID, HasFqn):
)
@dataclass
class MetricReference(dbtClassMixin, Replaceable):
sql: Optional[Union[str, int]]
unique_id: Optional[str]
@dataclass
class ParsedMetric(UnparsedBaseNode, HasUniqueID, HasFqn):
model: str
name: str
description: str
label: str
type: str
sql: Optional[str]
sql: str
timestamp: Optional[str]
filters: List[MetricFilter]
time_grains: List[str]
dimensions: List[str]
model: Optional[str] = None
model_unique_id: Optional[str] = None
resource_type: NodeType = NodeType.Metric
meta: Dict[str, Any] = field(default_factory=dict)
tags: List[str] = field(default_factory=list)
sources: List[List[str]] = field(default_factory=list)
depends_on: DependsOn = field(default_factory=DependsOn)
refs: List[List[str]] = field(default_factory=list)
metrics: List[List[str]] = field(default_factory=list)
created_at: float = field(default_factory=lambda: time.time())
@property

View File

@@ -1,15 +1,11 @@
from dbt.node_types import NodeType
from dbt.contracts.util import (
AdditionalPropertiesMixin,
Mergeable,
Replaceable,
)
from dbt.contracts.util import AdditionalPropertiesMixin, Mergeable, Replaceable
# trigger the PathEncoder
import dbt.helper_types # noqa:F401
from dbt.exceptions import CompilationException, ParsingException
from dbt.dataclass_schema import dbtClassMixin, StrEnum, ExtensibleDbtClassMixin
from dbt.dataclass_schema import dbtClassMixin, StrEnum, ExtensibleDbtClassMixin, ValidationError
from dataclasses import dataclass, field
from datetime import timedelta
@@ -80,6 +76,7 @@ class UnparsedRunHook(UnparsedNode):
@dataclass
class Docs(dbtClassMixin, Replaceable):
show: bool = True
node_color: Optional[str] = None
@dataclass
@@ -448,12 +445,15 @@ class MetricFilter(dbtClassMixin, Replaceable):
@dataclass
class UnparsedMetric(dbtClassMixin, Replaceable):
model: str
# TODO : verify that this disallows metric names with spaces
# TODO: fix validation that you broke :p
# name: Identifier
name: str
label: str
type: str
model: Optional[str] = None
description: str = ""
sql: Optional[str] = None
sql: Union[str, int] = ""
timestamp: Optional[str] = None
time_grains: List[str] = field(default_factory=list)
dimensions: List[str] = field(default_factory=list)
@@ -463,6 +463,15 @@ class UnparsedMetric(dbtClassMixin, Replaceable):
@classmethod
def validate(cls, data):
# super().validate(data)
# TODO: putting this back for now to get tests passing. Do we want to implement name: Identifier?
super(UnparsedMetric, cls).validate(data)
if "name" in data and " " in data["name"]:
raise ParsingException(f"Metrics name '{data['name']}' cannot contain spaces")
# TODO: Expressions _cannot_ have `model` properties
if data.get("model") is None and data.get("type") != "expression":
raise ValidationError("Non-expression metrics require a 'model' property")
if data.get("model") is not None and data.get("type") == "expression":
raise ValidationError("Expression metrics cannot have a 'model' property")

View File

@@ -0,0 +1,153 @@
import re
HTML_COLORS = [
"aliceblue",
"antiquewhite",
"aqua",
"aquamarine",
"azure",
"beige",
"bisque",
"black",
"blanchedalmond",
"blue",
"blueviolet",
"brown",
"burlywood",
"cadetblue",
"chartreuse",
"chocolate",
"coral",
"cornflowerblue",
"cornsilk",
"crimson",
"cyan",
"darkblue",
"darkcyan",
"darkgoldenrod",
"darkgray",
"darkgreen",
"darkkhaki",
"darkmagenta",
"darkolivegreen",
"darkorange",
"darkorchid",
"darkred",
"darksalmon",
"darkseagreen",
"darkslateblue",
"darkslategray",
"darkturquoise",
"darkviolet",
"deeppink",
"deepskyblue",
"dimgray",
"dodgerblue",
"firebrick",
"floralwhite",
"forestgreen",
"fuchsia",
"gainsboro",
"ghostwhite",
"gold",
"goldenrod",
"gray",
"green",
"greenyellow",
"honeydew",
"hotpink",
"indianred",
"indigo",
"ivory",
"khaki",
"lavender",
"lavenderblush",
"lawngreen",
"lemonchiffon",
"lightblue",
"lightcoral",
"lightcyan",
"lightgoldenrodyellow",
"lightgray",
"lightgreen",
"lightpink",
"lightsalmon",
"lightsalmon",
"lightseagreen",
"lightskyblue",
"lightslategray",
"lightsteelblue",
"lightyellow",
"lime",
"limegreen",
"linen",
"magenta",
"maroon",
"mediumaquamarine",
"mediumblue",
"mediumorchid",
"mediumpurple",
"mediumseagreen",
"mediumslateblue",
"mediumslateblue",
"mediumspringgreen",
"mediumturquoise",
"mediumvioletred",
"midnightblue",
"mintcream",
"mistyrose",
"moccasin",
"navajowhite",
"navy",
"oldlace",
"olive",
"olivedrab",
"orange",
"orangered",
"orchid",
"palegoldenrod",
"palegreen",
"paleturquoise",
"palevioletred",
"papayawhip",
"peachpuff",
"peru",
"pink",
"plum",
"powderblue",
"purple",
"rebeccapurple",
"red",
"rosybrown",
"royalblue",
"saddlebrown",
"salmon",
"sandybrown",
"seagreen",
"seashell",
"sienna",
"silver",
"skyblue",
"slateblue",
"slategray",
"snow",
"springgreen",
"steelblue",
"tan",
"teal",
"thistle",
"tomato",
"turquoise",
"violet",
"wheat",
"white",
"whitesmoke",
"yellow",
"yellowgreen",
]
def validate_color(color: str) -> bool:
match_hex = re.search(r"^#(?:[0-9a-f]{3}){1,2}$", color.lower())
match_html_color_name = color.lower() in HTML_COLORS
return bool(match_hex or match_html_color_name)

View File

@@ -1,4 +1,4 @@
from dbt.contracts.util import Replaceable, Mergeable, list_str
from dbt.contracts.util import Replaceable, Mergeable, list_str, Identifier
from dbt.contracts.connection import QueryComment, UserConfigContract
from dbt.helper_types import NoValue
from dbt.dataclass_schema import (
@@ -7,7 +7,6 @@ from dbt.dataclass_schema import (
HyphenatedDbtClassMixin,
ExtensibleDbtClassMixin,
register_pattern,
ValidatedStringMixin,
)
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Union, Any
@@ -19,25 +18,6 @@ PIN_PACKAGE_URL = (
DEFAULT_SEND_ANONYMOUS_USAGE_STATS = True
class Name(ValidatedStringMixin):
ValidationRegex = r"^[^\d\W]\w*$"
@classmethod
def is_valid(cls, value: Any) -> bool:
if not isinstance(value, str):
return False
try:
cls.validate(value)
except ValidationError:
return False
return True
register_pattern(Name, r"^[^\d\W]\w*$")
class SemverString(str, SerializableType):
def _serialize(self) -> str:
return self
@@ -182,7 +162,7 @@ BANNED_PROJECT_NAMES = {
@dataclass
class Project(HyphenatedDbtClassMixin, Replaceable):
name: Name
name: Identifier
version: Union[SemverString, float]
config_version: int
project_root: Optional[str] = None

View File

@@ -9,6 +9,13 @@ from dbt.version import __version__
from dbt.events.functions import get_invocation_id
from dbt.dataclass_schema import dbtClassMixin
from dbt.dataclass_schema import (
ValidatedStringMixin,
ValidationError,
register_pattern,
)
SourceKey = Tuple[str, str]
@@ -201,6 +208,14 @@ class VersionedSchema(dbtClassMixin):
result["$id"] = str(cls.dbt_schema_version)
return result
@classmethod
def is_compatible_version(cls, schema_version):
compatible_versions = [str(cls.dbt_schema_version)]
if hasattr(cls, "compatible_previous_versions"):
for name, version in cls.compatible_previous_versions():
compatible_versions.append(str(SchemaVersion(name, version)))
return str(schema_version) in compatible_versions
@classmethod
def read_and_check_versions(cls, path: str):
try:
@@ -217,7 +232,7 @@ class VersionedSchema(dbtClassMixin):
if "metadata" in data and "dbt_schema_version" in data["metadata"]:
previous_schema_version = data["metadata"]["dbt_schema_version"]
# cls.dbt_schema_version is a SchemaVersion object
if str(cls.dbt_schema_version) != previous_schema_version:
if not cls.is_compatible_version(previous_schema_version):
raise IncompatibleSchemaException(
expected=str(cls.dbt_schema_version), found=previous_schema_version
)
@@ -242,3 +257,22 @@ class ArtifactMixin(VersionedSchema, Writable, Readable):
super().validate(data)
if cls.dbt_schema_version is None:
raise InternalException("Cannot call from_dict with no schema version!")
class Identifier(ValidatedStringMixin):
ValidationRegex = r"^[^\d\W]\w*$"
@classmethod
def is_valid(cls, value: Any) -> bool:
if not isinstance(value, str):
return False
try:
cls.validate(value)
except ValidationError:
return False
return True
register_pattern(Identifier, r"^[^\d\W]\w*$")

View File

@@ -103,7 +103,8 @@ SomeUnpinned = TypeVar("SomeUnpinned", bound="UnpinnedPackage")
class UnpinnedPackage(Generic[SomePinned], BasePackage):
@abc.abstractclassmethod
@classmethod
@abc.abstractmethod
def from_contract(cls, contract):
raise NotImplementedError

View File

@@ -1,4 +1,3 @@
import colorama
from colorama import Style
import dbt.events.functions as this # don't worry I hate it too.
from dbt.events.base_types import NoStdOut, Event, NoFile, ShowException, Cache
@@ -50,14 +49,6 @@ format_color = True
format_json = False
invocation_id: Optional[str] = None
# Colorama needs some help on windows because we're using logger.info
# intead of print(). If the Windows env doesn't have a TERM var set,
# then we should override the logging stream to use the colorama
# converter. If the TERM var is set (as with Git Bash), then it's safe
# to send escape characters and no log handler injection is needed.
if sys.platform == "win32":
colorama.init(wrap=False)
def setup_event_logger(log_path, level_override=None):
# flags have been resolved, and log_path is known
@@ -175,8 +166,12 @@ def event_to_serializable_dict(
# translates an Event to a completely formatted text-based log line
# type hinting everything as strings so we don't get any unintentional string conversions via str()
def reset_color() -> str:
return "" if not this.format_color else Style.RESET_ALL
def create_info_text_log_line(e: T_Event) -> str:
color_tag: str = "" if this.format_color else Style.RESET_ALL
color_tag: str = reset_color()
ts: str = get_ts().strftime("%H:%M:%S")
scrubbed_msg: str = scrub_secrets(e.message(), env_secrets())
log_line: str = f"{color_tag}{ts} {scrubbed_msg}"
@@ -189,7 +184,7 @@ def create_debug_text_log_line(e: T_Event) -> str:
if type(e) == MainReportVersion:
separator = 30 * "="
log_line = f"\n\n{separator} {get_ts()} | {get_invocation_id()} {separator}\n"
color_tag: str = "" if this.format_color else Style.RESET_ALL
color_tag: str = reset_color()
ts: str = get_ts().strftime("%H:%M:%S.%f")
scrubbed_msg: str = scrub_secrets(e.message(), env_secrets())
level: str = e.level_tag() if len(e.level_tag()) == 5 else f"{e.level_tag()} "

View File

@@ -1501,10 +1501,11 @@ class HooksRunning(InfoLevel):
class HookFinished(InfoLevel):
stat_line: str
execution: str
execution_time: float
code: str = "E040"
def message(self) -> str:
return f"Finished running {self.stat_line}{self.execution}."
return f"Finished running {self.stat_line}{self.execution} ({self.execution_time:0.2f}s)."
@dataclass
@@ -2352,11 +2353,15 @@ class WritingInjectedSQLForNode(DebugLevel):
@dataclass
class DisableTracking(WarnLevel):
class DisableTracking(DebugLevel):
code: str = "Z039"
def message(self) -> str:
return "Error sending message, disabling tracking"
return (
"Error sending anonymous usage statistics. Disabling tracking for this execution. "
"If you wish to permanently disable tracking, see: "
"https://docs.getdbt.com/reference/global-configs#send-anonymous-usage-stats."
)
@dataclass
@@ -2417,9 +2422,7 @@ class GeneralWarningMsg(WarnLevel):
code: str = "Z046"
def message(self) -> str:
if self.log_fmt is not None:
return self.log_fmt.format(self.msg)
return self.msg
return self.log_fmt.format(self.msg) if self.log_fmt is not None else self.msg
@dataclass
@@ -2429,9 +2432,7 @@ class GeneralWarningException(WarnLevel):
code: str = "Z047"
def message(self) -> str:
if self.log_fmt is not None:
return self.log_fmt.format(str(self.exc))
return str(self.exc)
return self.log_fmt.format(str(self.exc)) if self.log_fmt is not None else str(self.exc)
@dataclass
@@ -2620,7 +2621,7 @@ if 1 == 0:
DatabaseErrorRunning(hook_type="")
EmptyLine()
HooksRunning(num_hooks=0, hook_type="")
HookFinished(stat_line="", execution="")
HookFinished(stat_line="", execution="", execution_time=0)
WriteCatalogFailure(num_exceptions=0)
CatalogWritten(path="")
CannotGenerateDocs()

View File

@@ -520,6 +520,12 @@ def ref_invalid_args(model, args) -> NoReturn:
raise_compiler_error("ref() takes at most two arguments ({} given)".format(len(args)), model)
def metric_invalid_args(model, args) -> NoReturn:
raise_compiler_error(
"metric() takes at most two arguments ({} given)".format(len(args)), model
)
def ref_bad_context(model, args) -> NoReturn:
ref_args = ", ".join("'{}'".format(a) for a in args)
ref_string = "{{{{ ref({}) }}}}".format(ref_args)
@@ -650,6 +656,23 @@ def source_target_not_found(
raise_compiler_error(msg, model)
def get_metric_not_found_msg(
model,
target_name: str,
target_package: Optional[str],
) -> str:
reason = "was not found"
return _get_target_failure_msg(
model, target_name, target_package, include_path=True, reason=reason, target_kind="metric"
)
def metric_target_not_found(metric, target_name: str, target_package: Optional[str]) -> NoReturn:
msg = get_metric_not_found_msg(metric, target_name, target_package)
raise_compiler_error(msg, metric)
def dependency_not_found(model, target_model_name):
raise_compiler_error(
"'{}' depends on '{}' which is not in the graph!".format(

View File

@@ -37,6 +37,8 @@ EVENT_BUFFER_SIZE = 100000
QUIET = None
NO_PRINT = None
CACHE_SELECTED_ONLY = None
TARGET_PATH = None
LOG_PATH = None
_NON_BOOLEAN_FLAGS = [
"LOG_FORMAT",
@@ -44,6 +46,8 @@ _NON_BOOLEAN_FLAGS = [
"PROFILES_DIR",
"INDIRECT_SELECTION",
"EVENT_BUFFER_SIZE",
"TARGET_PATH",
"LOG_PATH",
]
_NON_DBT_ENV_FLAGS = ["DO_NOT_TRACK"]
@@ -71,6 +75,8 @@ flag_defaults = {
"QUIET": False,
"NO_PRINT": False,
"CACHE_SELECTED_ONLY": False,
"TARGET_PATH": None,
"LOG_PATH": None,
}
@@ -121,6 +127,7 @@ def set_from_args(args, user_config):
global WRITE_JSON, PARTIAL_PARSE, USE_COLORS, STORE_FAILURES, PROFILES_DIR, DEBUG, LOG_FORMAT
global INDIRECT_SELECTION, VERSION_CHECK, FAIL_FAST, SEND_ANONYMOUS_USAGE_STATS
global PRINTER_WIDTH, WHICH, LOG_CACHE_EVENTS, EVENT_BUFFER_SIZE, QUIET, NO_PRINT, CACHE_SELECTED_ONLY
global TARGET_PATH, LOG_PATH
STRICT_MODE = False # backwards compatibility
# cli args without user_config or env var option
@@ -148,6 +155,8 @@ def set_from_args(args, user_config):
QUIET = get_flag_value("QUIET", args, user_config)
NO_PRINT = get_flag_value("NO_PRINT", args, user_config)
CACHE_SELECTED_ONLY = get_flag_value("CACHE_SELECTED_ONLY", args, user_config)
TARGET_PATH = get_flag_value("TARGET_PATH", args, user_config)
LOG_PATH = get_flag_value("LOG_PATH", args, user_config)
_set_overrides_from_env()

View File

@@ -28,20 +28,16 @@ class Graph:
"""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!")
# This used to use nx.utils.reversed(self.graph), but that is deprecated,
# so changing to use self.graph.reverse(copy=False) as recommeneded
G = self.graph.reverse(copy=False) if self.graph.is_directed() else self.graph
anc = nx.single_source_shortest_path_length(G=G, source=node, cutoff=max_depth).keys()
return anc - {node}
return {
child
for _, child in nx.bfs_edges(self.graph, node, reverse=True, depth_limit=max_depth)
}
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}
return {child for _, child in nx.bfs_edges(self.graph, node, depth_limit=max_depth)}
def select_childrens_parents(self, selected: Set[UniqueId]) -> Set[UniqueId]:
ancestors_for = self.select_children(selected) | selected

View File

@@ -39,6 +39,7 @@ class MethodName(StrEnum):
Tag = "tag"
Source = "source"
Path = "path"
File = "file"
Package = "package"
Config = "config"
TestName = "test_name"
@@ -280,7 +281,7 @@ class MetricSelectorMethod(SelectorMethod):
class PathSelectorMethod(SelectorMethod):
def search(self, included_nodes: Set[UniqueId], selector: str) -> Iterator[UniqueId]:
"""Yields nodes from inclucded that match the given path."""
"""Yields nodes from included 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))
@@ -294,6 +295,14 @@ class PathSelectorMethod(SelectorMethod):
yield node
class FileSelectorMethod(SelectorMethod):
def search(self, included_nodes: Set[UniqueId], selector: str) -> Iterator[UniqueId]:
"""Yields nodes from included that match the given file name."""
for node, real_node in self.all_nodes(included_nodes):
if Path(real_node.original_file_path).name == selector:
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"""
@@ -416,25 +425,31 @@ class StateSelectorMethod(SelectorMethod):
return modified
def recursively_check_macros_modified(self, node, visited_macros):
# loop through all macros that this node depends on
for macro_uid in node.depends_on.macros:
# avoid infinite recursion if we've already seen this macro
if macro_uid in visited_macros:
continue
visited_macros.append(macro_uid)
# 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
# this macro hasn't been modified, but depends on other
# macros which each need to be tested for modification
macro_node = self.manifest.macros[macro_uid]
if len(macro_node.depends_on.macros) > 0:
return self.recursively_check_macros_modified(macro_node, visited_macros)
upstream_macros_changed = self.recursively_check_macros_modified(
macro_node, visited_macros
)
if upstream_macros_changed:
return True
continue
# this macro hasn't been modified, but we haven't checked
# the other macros the node depends on, so keep looking
elif len(node.depends_on.macros) > len(visited_macros):
if len(node.depends_on.macros) > len(visited_macros):
continue
else:
return False
return False
def check_macros_modified(self, node):
# check if there are any changes in macros the first time
@@ -540,7 +555,7 @@ class SourceStatusSelectorMethod(SelectorMethod):
)
current_state_sources = {
result.unique_id: getattr(result, "max_loaded_at", None)
result.unique_id: getattr(result, "max_loaded_at", 0)
for result in self.previous_state.sources_current.results
if hasattr(result, "max_loaded_at")
}
@@ -552,7 +567,7 @@ class SourceStatusSelectorMethod(SelectorMethod):
}
previous_state_sources = {
result.unique_id: getattr(result, "max_loaded_at", None)
result.unique_id: getattr(result, "max_loaded_at", 0)
for result in self.previous_state.sources.results
if hasattr(result, "max_loaded_at")
}
@@ -589,6 +604,7 @@ class MethodManager:
MethodName.Tag: TagSelectorMethod,
MethodName.Source: SourceSelectorMethod,
MethodName.Path: PathSelectorMethod,
MethodName.File: FileSelectorMethod,
MethodName.Package: PackageSelectorMethod,
MethodName.Config: ConfigSelectorMethod,
MethodName.TestName: TestNameSelectorMethod,

View File

@@ -2,7 +2,7 @@ import os
import re
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass
from dbt.dataclass_schema import StrEnum
from dbt.dataclass_schema import StrEnum, dbtClassMixin
from typing import Set, Iterator, List, Optional, Dict, Union, Any, Iterable, Tuple
from .graph import UniqueId
@@ -80,6 +80,8 @@ class SelectionCriteria:
def default_method(cls, value: str) -> MethodName:
if _probably_path(value):
return MethodName.Path
elif value.lower().endswith(".sql"):
return MethodName.File
else:
return MethodName.FQN
@@ -167,7 +169,7 @@ class SelectionCriteria:
)
class BaseSelectionGroup(Iterable[SelectionSpec], metaclass=ABCMeta):
class BaseSelectionGroup(dbtClassMixin, Iterable[SelectionSpec], metaclass=ABCMeta):
def __init__(
self,
components: Iterable[SelectionSpec],

View File

@@ -0,0 +1,167 @@
{# ------- BOOLEAN MACROS --------- #}
{#
-- COPY GRANTS
-- When a relational object (view or table) is replaced in this database,
-- do previous grants carry over to the new object? This may depend on:
-- whether we use alter-rename-swap versus CREATE OR REPLACE
-- user-supplied configuration (e.g. copy_grants on Snowflake)
-- By default, play it safe, assume TRUE: that grants ARE copied over.
-- This means dbt will first "show" current grants and then calculate diffs.
-- It may require an additional query than is strictly necessary,
-- but better safe than sorry.
#}
{% macro copy_grants() %}
{{ return(adapter.dispatch('copy_grants', 'dbt')()) }}
{% endmacro %}
{% macro default__copy_grants() %}
{{ return(True) }}
{% endmacro %}
{#
-- SUPPORT MULTIPLE GRANTEES PER DCL STATEMENT
-- Does this database support 'grant {privilege} to {grantee_1}, {grantee_2}, ...'
-- Or must these be separate statements:
-- `grant {privilege} to {grantee_1}`;
-- `grant {privilege} to {grantee_2}`;
-- By default, pick the former, because it's what we prefer when available.
#}
{% macro support_multiple_grantees_per_dcl_statement() %}
{{ return(adapter.dispatch('support_multiple_grantees_per_dcl_statement', 'dbt')()) }}
{% endmacro %}
{%- macro default__support_multiple_grantees_per_dcl_statement() -%}
{{ return(True) }}
{%- endmacro -%}
{% macro should_revoke(existing_relation, full_refresh_mode=True) %}
{% if not existing_relation %}
{#-- The table doesn't already exist, so no grants to copy over --#}
{{ return(False) }}
{% elif full_refresh_mode %}
{#-- The object is being REPLACED -- whether grants are copied over depends on the value of user config --#}
{{ return(copy_grants()) }}
{% else %}
{#-- The table is being merged/upserted/inserted -- grants will be carried over --#}
{{ return(True) }}
{% endif %}
{% endmacro %}
{# ------- DCL STATEMENT TEMPLATES --------- #}
{% macro get_show_grant_sql(relation) %}
{{ return(adapter.dispatch("get_show_grant_sql", "dbt")(relation)) }}
{% endmacro %}
{% macro default__get_show_grant_sql(relation) %}
show grants on {{ relation }}
{% endmacro %}
{% macro get_grant_sql(relation, privilege, grantees) %}
{{ return(adapter.dispatch('get_grant_sql', 'dbt')(relation, privilege, grantees)) }}
{% endmacro %}
{%- macro default__get_grant_sql(relation, privilege, grantees) -%}
grant {{ privilege }} on {{ relation }} to {{ grantees | join(', ') }}
{%- endmacro -%}
{% macro get_revoke_sql(relation, privilege, grantees) %}
{{ return(adapter.dispatch('get_revoke_sql', 'dbt')(relation, privilege, grantees)) }}
{% endmacro %}
{%- macro default__get_revoke_sql(relation, privilege, grantees) -%}
revoke {{ privilege }} on {{ relation }} from {{ grantees | join(', ') }}
{%- endmacro -%}
{# ------- RUNTIME APPLICATION --------- #}
{% macro get_dcl_statement_list(relation, grant_config, get_dcl_macro) %}
{{ return(adapter.dispatch('get_dcl_statement_list', 'dbt')(relation, grant_config, get_dcl_macro)) }}
{% endmacro %}
{%- macro default__get_dcl_statement_list(relation, grant_config, get_dcl_macro) -%}
{#
-- Unpack grant_config into specific privileges and the set of users who need them granted/revoked.
-- Depending on whether this database supports multiple grantees per statement, pass in the list of
-- all grantees per privilege, or (if not) template one statement per privilege-grantee pair.
-- `get_dcl_macro` will be either `get_grant_sql` or `get_revoke_sql`
#}
{%- set dcl_statements = [] -%}
{%- for privilege, grantees in grant_config.items() %}
{%- if support_multiple_grantees_per_dcl_statement() and grantees -%}
{%- set dcl = get_dcl_macro(relation, privilege, grantees) -%}
{%- do dcl_statements.append(dcl) -%}
{%- else -%}
{%- for grantee in grantees -%}
{% set dcl = get_dcl_macro(relation, privilege, [grantee]) %}
{%- do dcl_statements.append(dcl) -%}
{% endfor -%}
{%- endif -%}
{%- endfor -%}
{{ return(dcl_statements) }}
{%- endmacro %}
{% macro call_dcl_statements(dcl_statement_list) %}
{{ return(adapter.dispatch("call_dcl_statements", "dbt")(dcl_statement_list)) }}
{% endmacro %}
{% macro default__call_dcl_statements(dcl_statement_list) %}
{#
-- By default, supply all grant + revoke statements in a single semicolon-separated block,
-- so that they're all processed together.
-- Some databases do not support this. Those adapters will need to override this macro
-- to run each statement individually.
#}
{% call statement('grants') %}
{% for dcl_statement in dcl_statement_list %}
{{ dcl_statement }};
{% endfor %}
{% endcall %}
{% endmacro %}
{% macro apply_grants(relation, grant_config, should_revoke) %}
{{ return(adapter.dispatch("apply_grants", "dbt")(relation, grant_config, should_revoke)) }}
{% endmacro %}
{% macro default__apply_grants(relation, grant_config, should_revoke=True) %}
{#-- If grant_config is {} or None, this is a no-op --#}
{% if grant_config %}
{% if should_revoke %}
{#-- We think previous grants may have carried over --#}
{#-- Show current grants and calculate diffs --#}
{% set current_grants_table = run_query(get_show_grant_sql(relation)) %}
{% set current_grants_dict = adapter.standardize_grants_dict(current_grants_table) %}
{% set needs_granting = diff_of_two_dicts(grant_config, current_grants_dict) %}
{% set needs_revoking = diff_of_two_dicts(current_grants_dict, grant_config) %}
{% if not (needs_granting or needs_revoking) %}
{{ log('On ' ~ relation ~': All grants are in place, no revocation or granting needed.')}}
{% endif %}
{% else %}
{#-- We don't think there's any chance of previous grants having carried over. --#}
{#-- Jump straight to granting what the user has configured. --#}
{% set needs_revoking = {} %}
{% set needs_granting = grant_config %}
{% endif %}
{% if needs_granting or needs_revoking %}
{% set revoke_statement_list = get_dcl_statement_list(relation, needs_revoking, get_revoke_sql) %}
{% set grant_statement_list = get_dcl_statement_list(relation, needs_granting, get_grant_sql) %}
{% set dcl_statement_list = revoke_statement_list + grant_statement_list %}
{% if dcl_statement_list %}
{{ call_dcl_statements(dcl_statement_list) }}
{% endif %}
{% endif %}
{% endif %}
{% endmacro %}

View File

@@ -1,15 +1,35 @@
{% macro make_intermediate_relation(base_relation, suffix='__dbt_tmp') %}
{{ return(adapter.dispatch('make_intermediate_relation', 'dbt')(base_relation, suffix)) }}
{% endmacro %}
{% macro default__make_intermediate_relation(base_relation, suffix) %}
{{ return(default__make_temp_relation(base_relation, suffix)) }}
{% endmacro %}
{% macro make_temp_relation(base_relation, suffix='__dbt_tmp') %}
{{ return(adapter.dispatch('make_temp_relation', 'dbt')(base_relation, suffix))}}
{{ return(adapter.dispatch('make_temp_relation', 'dbt')(base_relation, suffix)) }}
{% endmacro %}
{% macro default__make_temp_relation(base_relation, suffix) %}
{% set tmp_identifier = base_relation.identifier ~ suffix %}
{% set tmp_relation = base_relation.incorporate(
path={"identifier": tmp_identifier}) -%}
{%- set temp_identifier = base_relation.identifier ~ suffix -%}
{%- set temp_relation = base_relation.incorporate(
path={"identifier": temp_identifier}) -%}
{% do return(tmp_relation) %}
{{ return(temp_relation) }}
{% endmacro %}
{% macro make_backup_relation(base_relation, backup_relation_type, suffix='__dbt_backup') %}
{{ return(adapter.dispatch('make_backup_relation', 'dbt')(base_relation, backup_relation_type, suffix)) }}
{% endmacro %}
{% macro default__make_backup_relation(base_relation, backup_relation_type, suffix) %}
{%- set backup_identifier = base_relation.identifier ~ suffix -%}
{%- set backup_relation = base_relation.incorporate(
path={"identifier": backup_identifier},
type=backup_relation_type
) -%}
{{ return(backup_relation) }}
{% endmacro %}
{% macro drop_relation(relation) -%}
{{ return(adapter.dispatch('drop_relation', 'dbt')(relation)) }}
@@ -66,8 +86,8 @@
{% endmacro %}
{# a user-friendly interface into adapter.get_relation #}
{% macro load_relation(relation) %}
-- a user-friendly interface into adapter.get_relation
{% macro load_cached_relation(relation) %}
{% do return(adapter.get_relation(
database=relation.database,
schema=relation.schema,
@@ -75,8 +95,12 @@
)) -%}
{% endmacro %}
-- old name for backwards compatibility
{% macro load_relation(relation) %}
{{ return(load_cached_relation(relation)) }}
{% endmacro %}
{# not used much, here for backwards compatibility #}
{% macro drop_relation_if_exists(relation) %}
{% if relation is not none %}
{{ adapter.drop_relation(relation) }}

View File

@@ -1,28 +1,27 @@
{% materialization incremental, default -%}
{% set unique_key = config.get('unique_key') %}
-- relations
{%- set existing_relation = load_cached_relation(this) -%}
{%- set target_relation = this.incorporate(type='table') -%}
{%- set temp_relation = make_temp_relation(target_relation)-%}
{%- set intermediate_relation = make_intermediate_relation(target_relation)-%}
{%- set backup_relation_type = 'table' if existing_relation is none else existing_relation.type -%}
{%- set backup_relation = make_backup_relation(target_relation, backup_relation_type) -%}
{% set target_relation = this.incorporate(type='table') %}
{% set existing_relation = load_relation(this) %}
{% set tmp_relation = make_temp_relation(target_relation) %}
{%- set full_refresh_mode = (should_full_refresh()) -%}
-- configs
{%- set unique_key = config.get('unique_key') -%}
{%- set full_refresh_mode = (should_full_refresh() or existing_relation.is_view) -%}
{%- set on_schema_change = incremental_validate_on_schema_change(config.get('on_schema_change'), default='ignore') -%}
{% set on_schema_change = incremental_validate_on_schema_change(config.get('on_schema_change'), default='ignore') %}
{% set tmp_identifier = model['name'] + '__dbt_tmp' %}
{% set backup_identifier = model['name'] + "__dbt_backup" %}
-- the intermediate_ and backup_ relations should not already exist in the database; get_relation
-- the temp_ and backup_ relations should not already exist in the database; get_relation
-- will return None in that case. Otherwise, we get a relation that we can drop
-- later, before we try to use this name for the current operation. This has to happen before
-- BEGIN, in a separate transaction
{% set preexisting_intermediate_relation = adapter.get_relation(identifier=tmp_identifier,
schema=schema,
database=database) %}
{% set preexisting_backup_relation = adapter.get_relation(identifier=backup_identifier,
schema=schema,
database=database) %}
{%- set preexisting_intermediate_relation = load_cached_relation(intermediate_relation)-%}
{%- set preexisting_backup_relation = load_cached_relation(backup_relation) -%}
-- grab current tables grants config for comparision later on
{% set grant_config = config.get('grants') %}
{{ drop_relation_if_exists(preexisting_intermediate_relation) }}
{{ drop_relation_if_exists(preexisting_backup_relation) }}
@@ -33,32 +32,22 @@
{% set to_drop = [] %}
{# -- first check whether we want to full refresh for source view or config reasons #}
{% set trigger_full_refresh = (full_refresh_mode or existing_relation.is_view) %}
{% if existing_relation is none %}
{% set build_sql = create_table_as(False, target_relation, sql) %}
{% elif trigger_full_refresh %}
{#-- Make sure the backup doesn't exist so we don't encounter issues with the rename below #}
{% set tmp_identifier = model['name'] + '__dbt_tmp' %}
{% set backup_identifier = model['name'] + '__dbt_backup' %}
{% set intermediate_relation = existing_relation.incorporate(path={"identifier": tmp_identifier}) %}
{% set backup_relation = existing_relation.incorporate(path={"identifier": backup_identifier}) %}
{% set build_sql = create_table_as(False, intermediate_relation, sql) %}
{% set build_sql = get_create_table_as_sql(False, target_relation, sql) %}
{% elif full_refresh_mode %}
{% set build_sql = get_create_table_as_sql(False, intermediate_relation, sql) %}
{% set need_swap = true %}
{% do to_drop.append(backup_relation) %}
{% else %}
{% do run_query(create_table_as(True, tmp_relation, sql)) %}
{% do run_query(get_create_table_as_sql(True, temp_relation, sql)) %}
{% do adapter.expand_target_column_types(
from_relation=tmp_relation,
from_relation=temp_relation,
to_relation=target_relation) %}
{#-- Process schema changes. Returns dict of changes if successful. Use source columns for upserting/merging --#}
{% set dest_columns = process_schema_changes(on_schema_change, tmp_relation, existing_relation) %}
{% set dest_columns = process_schema_changes(on_schema_change, temp_relation, existing_relation) %}
{% if not dest_columns %}
{% set dest_columns = adapter.get_columns_in_relation(existing_relation) %}
{% endif %}
{% set build_sql = get_delete_insert_merge_sql(target_relation, tmp_relation, unique_key, dest_columns) %}
{% set build_sql = get_delete_insert_merge_sql(target_relation, temp_relation, unique_key, dest_columns) %}
{% endif %}
@@ -69,8 +58,12 @@
{% if need_swap %}
{% do adapter.rename_relation(target_relation, backup_relation) %}
{% do adapter.rename_relation(intermediate_relation, target_relation) %}
{% do to_drop.append(backup_relation) %}
{% endif %}
{% set should_revoke = should_revoke(existing_relation, full_refresh_mode) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
{% do persist_docs(target_relation, model) %}
{% if existing_relation is none or existing_relation.is_view or should_full_refresh() %}

View File

@@ -1,36 +1,21 @@
{% materialization table, default %}
{%- set identifier = model['alias'] -%}
{%- set tmp_identifier = model['name'] + '__dbt_tmp' -%}
{%- set backup_identifier = model['name'] + '__dbt_backup' -%}
{%- set old_relation = adapter.get_relation(database=database, schema=schema, identifier=identifier) -%}
{%- set target_relation = api.Relation.create(identifier=identifier,
schema=schema,
database=database,
type='table') -%}
{%- set intermediate_relation = api.Relation.create(identifier=tmp_identifier,
schema=schema,
database=database,
type='table') -%}
{%- set existing_relation = load_cached_relation(this) -%}
{%- set target_relation = this.incorporate(type='table') %}
{%- set intermediate_relation = make_intermediate_relation(target_relation) -%}
-- the intermediate_relation should not already exist in the database; get_relation
-- will return None in that case. Otherwise, we get a relation that we can drop
-- later, before we try to use this name for the current operation
{%- set preexisting_intermediate_relation = adapter.get_relation(identifier=tmp_identifier,
schema=schema,
database=database) -%}
{%- set preexisting_intermediate_relation = load_cached_relation(intermediate_relation) -%}
/*
See ../view/view.sql for more information about this relation.
*/
{%- set backup_relation_type = 'table' if old_relation is none else old_relation.type -%}
{%- set backup_relation = api.Relation.create(identifier=backup_identifier,
schema=schema,
database=database,
type=backup_relation_type) -%}
{%- set backup_relation_type = 'table' if existing_relation is none else existing_relation.type -%}
{%- set backup_relation = make_backup_relation(target_relation, backup_relation_type) -%}
-- as above, the backup_relation should not already exist
{%- set preexisting_backup_relation = adapter.get_relation(identifier=backup_identifier,
schema=schema,
database=database) -%}
{%- set preexisting_backup_relation = load_cached_relation(backup_relation) -%}
-- grab current tables grants config for comparision later on
{% set grant_config = config.get('grants') %}
-- drop the temp relations if they exist already in the database
{{ drop_relation_if_exists(preexisting_intermediate_relation) }}
@@ -47,8 +32,8 @@
{%- endcall %}
-- cleanup
{% if old_relation is not none %}
{{ adapter.rename_relation(old_relation, backup_relation) }}
{% if existing_relation is not none %}
{{ adapter.rename_relation(existing_relation, backup_relation) }}
{% endif %}
{{ adapter.rename_relation(intermediate_relation, target_relation) }}
@@ -57,6 +42,9 @@
{{ run_hooks(post_hooks, inside_transaction=True) }}
{% set should_revoke = should_revoke(existing_relation, full_refresh_mode=True) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
{% do persist_docs(target_relation, model) %}
-- `COMMIT` happens here

View File

@@ -13,12 +13,12 @@
{%- set identifier = model['alias'] -%}
{%- set old_relation = adapter.get_relation(database=database, schema=schema, identifier=identifier) -%}
{%- set exists_as_view = (old_relation is not none and old_relation.is_view) -%}
{%- set target_relation = api.Relation.create(
identifier=identifier, schema=schema, database=database,
type='view') -%}
{% set grant_config = config.get('grants') %}
{{ run_hooks(pre_hooks) }}
@@ -34,6 +34,9 @@
{{ get_create_view_as_sql(target_relation, sql) }}
{%- endcall %}
{% set should_revoke = should_revoke(exists_as_view, full_refresh_mode=True) %}
{% do apply_grants(target_relation, grant_config, should_revoke=True) %}
{{ run_hooks(post_hooks) }}
{{ return({'relations': [target_relation]}) }}

View File

@@ -1,41 +1,32 @@
{%- materialization view, default -%}
{%- set identifier = model['alias'] -%}
{%- set tmp_identifier = model['name'] + '__dbt_tmp' -%}
{%- set backup_identifier = model['name'] + '__dbt_backup' -%}
{%- set existing_relation = load_cached_relation(this) -%}
{%- set target_relation = this.incorporate(type='view') -%}
{%- set intermediate_relation = make_intermediate_relation(target_relation) -%}
{%- set old_relation = adapter.get_relation(database=database, schema=schema, identifier=identifier) -%}
{%- set target_relation = api.Relation.create(identifier=identifier, schema=schema, database=database,
type='view') -%}
{%- set intermediate_relation = api.Relation.create(identifier=tmp_identifier,
schema=schema, database=database, type='view') -%}
-- the intermediate_relation should not already exist in the database; get_relation
-- will return None in that case. Otherwise, we get a relation that we can drop
-- later, before we try to use this name for the current operation
{%- set preexisting_intermediate_relation = adapter.get_relation(identifier=tmp_identifier,
schema=schema,
database=database) -%}
{%- set preexisting_intermediate_relation = load_cached_relation(intermediate_relation) -%}
/*
This relation (probably) doesn't exist yet. If it does exist, it's a leftover from
a previous run, and we're going to try to drop it immediately. At the end of this
materialization, we're going to rename the "old_relation" to this identifier,
materialization, we're going to rename the "existing_relation" to this identifier,
and then we're going to drop it. In order to make sure we run the correct one of:
- drop view ...
- drop table ...
We need to set the type of this relation to be the type of the old_relation, if it exists,
or else "view" as a sane default if it does not. Note that if the old_relation does not
We need to set the type of this relation to be the type of the existing_relation, if it exists,
or else "view" as a sane default if it does not. Note that if the existing_relation does not
exist, then there is nothing to move out of the way and subsequentally drop. In that case,
this relation will be effectively unused.
*/
{%- set backup_relation_type = 'view' if old_relation is none else old_relation.type -%}
{%- set backup_relation = api.Relation.create(identifier=backup_identifier,
schema=schema, database=database,
type=backup_relation_type) -%}
{%- set backup_relation_type = 'view' if existing_relation is none else existing_relation.type -%}
{%- set backup_relation = make_backup_relation(target_relation, backup_relation_type) -%}
-- as above, the backup_relation should not already exist
{%- set preexisting_backup_relation = adapter.get_relation(identifier=backup_identifier,
schema=schema,
database=database) -%}
{%- set preexisting_backup_relation = load_cached_relation(backup_relation) -%}
-- grab current tables grants config for comparision later on
{% set grant_config = config.get('grants') %}
{{ run_hooks(pre_hooks, inside_transaction=False) }}
@@ -48,16 +39,19 @@
-- build model
{% call statement('main') -%}
{{ create_view_as(intermediate_relation, sql) }}
{{ get_create_view_as_sql(intermediate_relation, sql) }}
{%- endcall %}
-- cleanup
-- move the existing view out of the way
{% if old_relation is not none %}
{{ adapter.rename_relation(old_relation, backup_relation) }}
{% if existing_relation is not none %}
{{ adapter.rename_relation(existing_relation, backup_relation) }}
{% endif %}
{{ adapter.rename_relation(intermediate_relation, target_relation) }}
{% set should_revoke = should_revoke(existing_relation, full_refresh_mode=True) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
{% do persist_docs(target_relation, model) %}
{{ run_hooks(post_hooks, inside_transaction=True) }}

View File

@@ -8,7 +8,10 @@
{%- set exists_as_table = (old_relation is not none and old_relation.is_table) -%}
{%- set exists_as_view = (old_relation is not none and old_relation.is_view) -%}
{%- set grant_config = config.get('grants') -%}
{%- set agate_table = load_agate_table() -%}
-- grab current tables grants config for comparision later on
{%- do store_result('agate_table', response='OK', agate_table=agate_table) -%}
{{ run_hooks(pre_hooks, inside_transaction=False) }}
@@ -35,6 +38,10 @@
{% endcall %}
{% set target_relation = this.incorporate(type='table') %}
{% set should_revoke = should_revoke(old_relation, full_refresh_mode) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
{% do persist_docs(target_relation, model) %}
{% if full_refresh_mode or not exists_as_table %}

View File

@@ -169,13 +169,13 @@
{% macro build_snapshot_staging_table(strategy, sql, target_relation) %}
{% set tmp_relation = make_temp_relation(target_relation) %}
{% set temp_relation = make_temp_relation(target_relation) %}
{% set select = snapshot_staging_table(strategy, sql, target_relation) %}
{% call statement('build_snapshot_staging_relation') %}
{{ create_table_as(True, tmp_relation, select) }}
{{ create_table_as(True, temp_relation, select) }}
{% endcall %}
{% do return(tmp_relation) %}
{% do return(temp_relation) %}
{% endmacro %}

View File

@@ -5,6 +5,8 @@
{%- set strategy_name = config.get('strategy') -%}
{%- set unique_key = config.get('unique_key') %}
-- grab current tables grants config for comparision later on
{%- set grant_config = config.get('grants') -%}
{% set target_relation_exists, target_relation = get_or_create_relation(
database=model.database,
@@ -73,6 +75,9 @@
{{ final_sql }}
{% endcall %}
{% set should_revoke = should_revoke(target_relation_exists, full_refresh_mode=False) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
{% do persist_docs(target_relation, model) %}
{% if not target_relation_exists %}

View File

@@ -104,33 +104,41 @@
{% macro snapshot_check_all_get_existing_columns(node, target_exists, check_cols_config) -%}
{%- if not target_exists -%}
{#-- no table yet -> return whatever the query does --#}
{{ return((false, query_columns)) }}
{%- endif -%}
{#-- handle any schema changes --#}
{%- set target_relation = adapter.get_relation(database=node.database, schema=node.schema, identifier=node.alias) -%}
{% if check_cols_config == 'all' %}
{%- set query_columns = get_columns_in_query(node['compiled_sql']) -%}
{% elif check_cols_config is iterable and (check_cols_config | length) > 0 %}
{% set query_columns = check_cols_config %}
{#-- query for proper casing/quoting, to support comparison below --#}
{%- set select_check_cols_from_target -%}
select {{ check_cols_config | join(', ') }} from ({{ node['compiled_sql'] }}) subq
{%- endset -%}
{% set query_columns = get_columns_in_query(select_check_cols_from_target) %}
{% else %}
{% do exceptions.raise_compiler_error("Invalid value for 'check_cols': " ~ check_cols_config) %}
{% endif %}
{%- if not target_exists -%}
{# no table yet -> return whatever the query does #}
{{ return([false, query_columns]) }}
{%- endif -%}
{# handle any schema changes #}
{%- set target_table = node.get('alias', node.get('name')) -%}
{%- set target_relation = adapter.get_relation(database=node.database, schema=node.schema, identifier=target_table) -%}
{%- set existing_cols = get_columns_in_query('select * from ' ~ target_relation) -%}
{%- set ns = namespace() -%} {# handle for-loop scoping with a namespace #}
{%- set existing_cols = adapter.get_columns_in_relation(target_relation) | map(attribute = 'name') | list -%}
{%- set ns = namespace() -%} {#-- handle for-loop scoping with a namespace --#}
{%- set ns.column_added = false -%}
{%- set intersection = [] -%}
{%- for col in query_columns -%}
{%- if col in existing_cols -%}
{%- do intersection.append(col) -%}
{%- do intersection.append(adapter.quote(col)) -%}
{%- else -%}
{% set ns.column_added = true %}
{%- endif -%}
{%- endfor -%}
{{ return([ns.column_added, intersection]) }}
{{ return((ns.column_added, intersection)) }}
{%- endmacro %}

View File

@@ -0,0 +1,9 @@
{% macro any_value(expression) -%}
{{ return(adapter.dispatch('any_value', 'dbt') (expression)) }}
{% endmacro %}
{% macro default__any_value(expression) -%}
any_value({{ expression }})
{%- endmacro %}

View File

@@ -0,0 +1,9 @@
{% macro bool_or(expression) -%}
{{ return(adapter.dispatch('bool_or', 'dbt') (expression)) }}
{% endmacro %}
{% macro default__bool_or(expression) -%}
bool_or({{ expression }})
{%- endmacro %}

View File

@@ -0,0 +1,7 @@
{% macro cast_bool_to_text(field) %}
{{ adapter.dispatch('cast_bool_to_text', 'dbt') (field) }}
{% endmacro %}
{% macro default__cast_bool_to_text(field) %}
cast({{ field }} as {{ api.Column.translate_type('string') }})
{% endmacro %}

View File

@@ -0,0 +1,7 @@
{% macro concat(fields) -%}
{{ return(adapter.dispatch('concat', 'dbt')(fields)) }}
{%- endmacro %}
{% macro default__concat(fields) -%}
{{ fields|join(' || ') }}
{%- endmacro %}

View File

@@ -0,0 +1,117 @@
{# string ------------------------------------------------- #}
{%- macro type_string() -%}
{{ return(adapter.dispatch('type_string', 'dbt')()) }}
{%- endmacro -%}
{% macro default__type_string() %}
{{ return(api.Column.translate_type("string")) }}
{% endmacro %}
-- This will return 'text' by default
-- On Postgres + Snowflake, that's equivalent to varchar (no size)
-- Redshift will treat that as varchar(256)
{# timestamp ------------------------------------------------- #}
{%- macro type_timestamp() -%}
{{ return(adapter.dispatch('type_timestamp', 'dbt')()) }}
{%- endmacro -%}
{% macro default__type_timestamp() %}
{{ return(api.Column.translate_type("timestamp")) }}
{% endmacro %}
/*
POSTGRES
https://www.postgresql.org/docs/current/datatype-datetime.html:
The SQL standard requires that writing just `timestamp`
be equivalent to `timestamp without time zone`, and
PostgreSQL honors that behavior.
`timestamptz` is accepted as an abbreviation for `timestamp with time zone`;
this is a PostgreSQL extension.
SNOWFLAKE
https://docs.snowflake.com/en/sql-reference/data-types-datetime.html#timestamp
The TIMESTAMP_* variation associated with TIMESTAMP is specified by the
TIMESTAMP_TYPE_MAPPING session parameter. The default is TIMESTAMP_NTZ.
BIGQUERY
TIMESTAMP means 'timestamp with time zone'
DATETIME means 'timestamp without time zone'
TODO: shouldn't this return DATETIME instead of TIMESTAMP, for consistency with other databases?
e.g. dateadd returns a DATETIME
/* Snowflake:
https://docs.snowflake.com/en/sql-reference/data-types-datetime.html#timestamp
The TIMESTAMP_* variation associated with TIMESTAMP is specified by the TIMESTAMP_TYPE_MAPPING session parameter. The default is TIMESTAMP_NTZ.
*/
{# float ------------------------------------------------- #}
{%- macro type_float() -%}
{{ return(adapter.dispatch('type_float', 'dbt')()) }}
{%- endmacro -%}
{% macro default__type_float() %}
{{ return(api.Column.translate_type("float")) }}
{% endmacro %}
{# numeric ------------------------------------------------ #}
{%- macro type_numeric() -%}
{{ return(adapter.dispatch('type_numeric', 'dbt')()) }}
{%- endmacro -%}
/*
This one can't be just translate_type, since precision/scale make it a bit more complicated.
On most databases, the default (precision, scale) is something like:
Redshift: (18, 0)
Snowflake: (38, 0)
Postgres: (<=131072, 0)
https://www.postgresql.org/docs/current/datatype-numeric.html:
Specifying NUMERIC without any precision or scale creates an “unconstrained numeric”
column in which numeric values of any length can be stored, up to the implementation limits.
A column of this kind will not coerce input values to any particular scale,
whereas numeric columns with a declared scale will coerce input values to that scale.
(The SQL standard requires a default scale of 0, i.e., coercion to integer precision.
We find this a bit useless. If you're concerned about portability, always specify
the precision and scale explicitly.)
*/
{% macro default__type_numeric() %}
{{ return(api.Column.numeric_type("numeric", 28, 6)) }}
{% endmacro %}
{# bigint ------------------------------------------------- #}
{%- macro type_bigint() -%}
{{ return(adapter.dispatch('type_bigint', 'dbt')()) }}
{%- endmacro -%}
-- We don't have a conversion type for 'bigint' in TYPE_LABELS,
-- so this actually just returns the string 'bigint'
{% macro default__type_bigint() %}
{{ return(api.Column.translate_type("bigint")) }}
{% endmacro %}
-- Good news: BigQuery now supports 'bigint' (and 'int') as an alias for 'int64'
{# int ------------------------------------------------- #}
{%- macro type_int() -%}
{{ return(adapter.dispatch('type_int', 'dbt')()) }}
{%- endmacro -%}
{%- macro default__type_int() -%}
{{ return(api.Column.translate_type("integer")) }}
{%- endmacro -%}
-- returns 'int' everywhere, except BigQuery, where it returns 'int64'
-- (but BigQuery also now accepts 'int' as a valid alias for 'int64')

View File

@@ -0,0 +1,7 @@
{% macro date_trunc(datepart, date) -%}
{{ return(adapter.dispatch('date_trunc', 'dbt') (datepart, date)) }}
{%- endmacro %}
{% macro default__date_trunc(datepart, date) -%}
date_trunc('{{datepart}}', {{date}})
{%- endmacro %}

View File

@@ -0,0 +1,14 @@
{% macro dateadd(datepart, interval, from_date_or_timestamp) %}
{{ return(adapter.dispatch('dateadd', 'dbt')(datepart, interval, from_date_or_timestamp)) }}
{% endmacro %}
{% macro default__dateadd(datepart, interval, from_date_or_timestamp) %}
dateadd(
{{ datepart }},
{{ interval }},
{{ from_date_or_timestamp }}
)
{% endmacro %}

View File

@@ -0,0 +1,14 @@
{% macro datediff(first_date, second_date, datepart) %}
{{ return(adapter.dispatch('datediff', 'dbt')(first_date, second_date, datepart)) }}
{% endmacro %}
{% macro default__datediff(first_date, second_date, datepart) -%}
datediff(
{{ datepart }},
{{ first_date }},
{{ second_date }}
)
{%- endmacro %}

View File

@@ -0,0 +1,8 @@
{% macro escape_single_quotes(expression) %}
{{ return(adapter.dispatch('escape_single_quotes', 'dbt') (expression)) }}
{% endmacro %}
{# /*Default to replacing a single apostrophe with two apostrophes: they're -> they''re*/ #}
{% macro default__escape_single_quotes(expression) -%}
{{ expression | replace("'","''") }}
{%- endmacro %}

View File

@@ -0,0 +1,9 @@
{% macro except() %}
{{ return(adapter.dispatch('except', 'dbt')()) }}
{% endmacro %}
{% macro default__except() %}
except
{% endmacro %}

View File

@@ -0,0 +1,7 @@
{% macro hash(field) -%}
{{ return(adapter.dispatch('hash', 'dbt') (field)) }}
{%- endmacro %}
{% macro default__hash(field) -%}
md5(cast({{ field }} as {{ api.Column.translate_type('string') }}))
{%- endmacro %}

View File

@@ -0,0 +1,9 @@
{% macro intersect() %}
{{ return(adapter.dispatch('intersect', 'dbt')()) }}
{% endmacro %}
{% macro default__intersect() %}
intersect
{% endmacro %}

View File

@@ -0,0 +1,15 @@
{% macro last_day(date, datepart) %}
{{ return(adapter.dispatch('last_day', 'dbt') (date, datepart)) }}
{% endmacro %}
{%- macro default_last_day(date, datepart) -%}
cast(
{{dbt.dateadd('day', '-1',
dbt.dateadd(datepart, '1', dbt.date_trunc(datepart, date))
)}}
as date)
{%- endmacro -%}
{% macro default__last_day(date, datepart) -%}
{{dbt.default_last_day(date, datepart)}}
{%- endmacro %}

View File

@@ -0,0 +1,11 @@
{% macro length(expression) -%}
{{ return(adapter.dispatch('length', 'dbt') (expression)) }}
{% endmacro %}
{% macro default__length(expression) %}
length(
{{ expression }}
)
{%- endmacro -%}

View File

@@ -0,0 +1,30 @@
{% macro listagg(measure, delimiter_text="','", order_by_clause=none, limit_num=none) -%}
{{ return(adapter.dispatch('listagg', 'dbt') (measure, delimiter_text, order_by_clause, limit_num)) }}
{%- endmacro %}
{% macro default__listagg(measure, delimiter_text, order_by_clause, limit_num) -%}
{% if limit_num -%}
array_to_string(
array_slice(
array_agg(
{{ measure }}
){% if order_by_clause -%}
within group ({{ order_by_clause }})
{%- endif %}
,0
,{{ limit_num }}
),
{{ delimiter_text }}
)
{%- else %}
listagg(
{{ measure }},
{{ delimiter_text }}
)
{% if order_by_clause -%}
within group ({{ order_by_clause }})
{%- endif %}
{%- endif %}
{%- endmacro %}

View File

@@ -0,0 +1,7 @@
{%- macro string_literal(value) -%}
{{ return(adapter.dispatch('string_literal', 'dbt') (value)) }}
{%- endmacro -%}
{% macro default__string_literal(value) -%}
'{{ value }}'
{%- endmacro %}

View File

@@ -0,0 +1,11 @@
{% macro position(substring_text, string_text) -%}
{{ return(adapter.dispatch('position', 'dbt') (substring_text, string_text)) }}
{% endmacro %}
{% macro default__position(substring_text, string_text) %}
position(
{{ substring_text }} in {{ string_text }}
)
{%- endmacro -%}

View File

@@ -0,0 +1,14 @@
{% macro replace(field, old_chars, new_chars) -%}
{{ return(adapter.dispatch('replace', 'dbt') (field, old_chars, new_chars)) }}
{% endmacro %}
{% macro default__replace(field, old_chars, new_chars) %}
replace(
{{ field }},
{{ old_chars }},
{{ new_chars }}
)
{% endmacro %}

View File

@@ -0,0 +1,12 @@
{% macro right(string_text, length_expression) -%}
{{ return(adapter.dispatch('right', 'dbt') (string_text, length_expression)) }}
{% endmacro %}
{% macro default__right(string_text, length_expression) %}
right(
{{ string_text }},
{{ length_expression }}
)
{%- endmacro -%}

View File

@@ -0,0 +1,9 @@
{% macro safe_cast(field, type) %}
{{ return(adapter.dispatch('safe_cast', 'dbt') (field, type)) }}
{% endmacro %}
{% macro default__safe_cast(field, type) %}
{# most databases don't support this function yet
so we just need to use cast #}
cast({{field}} as {{type}})
{% endmacro %}

View File

@@ -0,0 +1,26 @@
{% macro split_part(string_text, delimiter_text, part_number) %}
{{ return(adapter.dispatch('split_part', 'dbt') (string_text, delimiter_text, part_number)) }}
{% endmacro %}
{% macro default__split_part(string_text, delimiter_text, part_number) %}
split_part(
{{ string_text }},
{{ delimiter_text }},
{{ part_number }}
)
{% endmacro %}
{% macro _split_part_negative(string_text, delimiter_text, part_number) %}
split_part(
{{ string_text }},
{{ delimiter_text }},
length({{ string_text }})
- length(
replace({{ string_text }}, {{ delimiter_text }}, '')
) + 2 {{ part_number }}
)
{% endmacro %}

File diff suppressed because one or more lines are too long

View File

@@ -15,17 +15,16 @@ import colorama
import logbook
from dbt.dataclass_schema import dbtClassMixin
# Colorama needs some help on windows because we're using logger.info
# intead of print(). If the Windows env doesn't have a TERM var set,
# then we should override the logging stream to use the colorama
# converter. If the TERM var is set (as with Git Bash), then it's safe
# to send escape characters and no log handler injection is needed.
logging_stdout = sys.stdout
if sys.platform == "win32":
if not os.getenv("TERM"):
logging_stdout = colorama.AnsiToWin32(sys.stdout).stream
colorama.init(wrap=False)
# Colorama is needed for colored logs on Windows because we're using logger.info
# intead of print(). If the Windows env doesn't have a TERM var set or it is set to None
# (i.e. in the case of Git Bash on Windows- this emulates Unix), then it's safe to initialize
# Colorama with wrapping turned on which allows us to strip ANSI sequences from stdout.
# You can safely initialize Colorama for any OS and the coloring stays the same except
# when piped to anoter process for Linux and MacOS, then it loses the coloring. To combat
# that, we will just initialize Colorama when needed on Windows using a non-Unix terminal.
if sys.platform == "win32" and (not os.getenv("TERM") or os.getenv("TERM") == "None"):
colorama.init(wrap=True)
STDOUT_LOG_FORMAT = "{record.message}"
DEBUG_LOG_FORMAT = (
@@ -455,7 +454,7 @@ class DelayedFileHandler(logbook.RotatingFileHandler, FormatterMixin):
class LogManager(logbook.NestedSetup):
def __init__(self, stdout=logging_stdout, stderr=sys.stderr):
def __init__(self, stdout=sys.stdout, stderr=sys.stderr):
self.stdout = stdout
self.stderr = stderr
self._null_handler = logbook.NullHandler()

View File

@@ -142,6 +142,7 @@ def main(args=None):
exit_code = e.code
except BaseException as e:
traceback.print_exc()
fire_event(MainEncounteredError(e=str(e)))
fire_event(MainStackTrace(stack_trace=traceback.format_exc()))
exit_code = ExitCodes.UnhandledError.value
@@ -651,6 +652,22 @@ def _add_common_arguments(*subparsers):
settings in profiles.yml.
""",
)
sub.add_argument(
"--target-path",
required=False,
help="""
Configure the 'target-path'. Only applies this setting for the
current run. Overrides the 'DBT_TARGET_PATH' if it is set.
""",
)
sub.add_argument(
"--log-path",
required=False,
help="""
Configure the 'log-path'. Only applies this setting for the
current run. Overrides the 'DBT_LOG_PATH' if it is set.
""",
)
_add_version_check(sub)

View File

@@ -12,8 +12,8 @@ class NodeType(StrEnum):
Seed = "seed"
# TODO: rm?
RPCCall = "rpc"
SqlOperation = "sql"
Documentation = "docs"
SqlOperation = "sql operation"
Documentation = "docs block"
Source = "source"
Macro = "macro"
Exposure = "exposure"
@@ -55,10 +55,9 @@ class NodeType(StrEnum):
]
def pluralize(self) -> str:
if self == "analysis":
if self is self.Analysis:
return "analyses"
else:
return f"{self}s"
return f"{self}s"
class RunHookType(StrEnum):

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