Compare commits

..

16 Commits

Author SHA1 Message Date
Dave Connors
ac7c8209cc remove trace 2022-10-26 09:12:53 -05:00
Dave Connors
d545c47112 parse list of dimensions to dictionary 2022-10-26 08:26:22 -05:00
Dave Connors
3cd5595482 dimensions 2022-10-25 08:18:16 -05:00
Dave Connors
d8f3586e56 process inverse relationships 2022-10-24 14:00:12 -05:00
Dave Connors
ab9d0fec55 process inverse relationships -- broken manifest 2022-10-14 15:52:17 -05:00
Dave Connors
f7255f2598 add optional metric config allow_joins to bypass relationship logic 2022-10-14 12:01:01 -05:00
Dave Connors
2e310d6e01 dimensions as dictionaries take 1 2022-10-13 15:40:59 -05:00
Dave Connors
1cd996a2c8 dataype, partial parsing support for model schema changes 2022-10-13 14:47:17 -05:00
Dave Connors
335b3062be Merge branch 'main' into simple-entity-attributes 2022-10-12 16:14:25 -05:00
Dave Connors
ce0c706102 add related model to depends on 2022-10-11 16:52:21 -05:00
Dave Connors
b18139497f dirty dimension inheritance from joined models 2022-10-06 14:52:02 -05:00
Dave Connors
ad5570cd6c allow all dimensions to be passed to metric object 2022-10-06 14:12:43 -05:00
Dave Connors
ef9f4d6cf9 futile attempt to check for node dimensions 2022-10-06 10:05:54 -05:00
Dave Connors
70f7dc24ce add datatype column attribute 2022-10-06 09:21:40 -05:00
Dave Connors
206b220e3d simple relationships for models 2022-10-06 09:11:18 -05:00
Dave Connors
5ed168c47b add is entity as top level model attribute, is_entity_dimension and is_primary_key as column attributes 2022-10-06 08:47:36 -05:00
572 changed files with 29809 additions and 28145 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 1.5.0a1 current_version = 1.4.0a1
parse = (?P<major>\d+) parse = (?P<major>\d+)
\.(?P<minor>\d+) \.(?P<minor>\d+)
\.(?P<patch>\d+) \.(?P<patch>\d+)

View File

@@ -3,7 +3,6 @@
For information on prior major and minor releases, see their changelogs: For information on prior major and minor releases, see their changelogs:
* [1.4](https://github.com/dbt-labs/dbt-core/blob/1.4.latest/CHANGELOG.md)
* [1.3](https://github.com/dbt-labs/dbt-core/blob/1.3.latest/CHANGELOG.md) * [1.3](https://github.com/dbt-labs/dbt-core/blob/1.3.latest/CHANGELOG.md)
* [1.2](https://github.com/dbt-labs/dbt-core/blob/1.2.latest/CHANGELOG.md) * [1.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.1](https://github.com/dbt-labs/dbt-core/blob/1.1.latest/CHANGELOG.md)

View File

@@ -0,0 +1,7 @@
kind: "Dependency"
body: "Bump black from 22.8.0 to 22.10.0"
time: 2022-10-07T00:08:48.00000Z
custom:
Author: dependabot[bot]
Issue: 4904
PR: 6019

View File

@@ -0,0 +1,7 @@
kind: Docs
body: minor doc correction
time: 2022-09-08T15:41:57.689162-04:00
custom:
Author: andy-clapson
Issue: "5791"
PR: "5684"

View File

@@ -0,0 +1,7 @@
kind: Docs
body: Generate API docs for new CLI interface
time: 2022-10-07T09:06:56.446078-05:00
custom:
Author: stu-k
Issue: "5528"
PR: "6022"

View File

@@ -0,0 +1,7 @@
kind: Features
body: Friendlier error messages when packages.yml is malformed
time: 2022-09-12T12:59:35.121188+01:00
custom:
Author: jared-rimmer
Issue: "5486"
PR: "5812"

View File

@@ -0,0 +1,7 @@
kind: Features
body: Migrate dbt-utils current_timestamp macros into core + adapters
time: 2022-09-14T09:56:25.97818-07:00
custom:
Author: colin-rogers-dbt
Issue: "5521"
PR: "5838"

View File

@@ -0,0 +1,7 @@
kind: Features
body: extend -f flag shorthand for seed command
time: 2022-10-03T11:07:05.381632-05:00
custom:
Author: dave-connors-3
Issue: "5990"
PR: "5991"

View File

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

View File

@@ -0,0 +1,7 @@
kind: Fixes
body: Account for disabled flags on models in schema files more completely
time: 2022-09-16T10:48:54.162273-05:00
custom:
Author: emmyoop
Issue: "3992"
PR: "5868"

View File

@@ -0,0 +1,7 @@
kind: Fixes
body: Add validation of enabled config for metrics, exposures and sources
time: 2022-10-10T11:32:18.752322-05:00
custom:
Author: emmyoop
Issue: "6030"
PR: "6038"

View File

@@ -0,0 +1,7 @@
kind: Fixes
body: check length of args of python model function before accessing it
time: 2022-10-11T16:07:15.464093-04:00
custom:
Author: chamini2
Issue: "6041"
PR: "6042"

View File

@@ -0,0 +1,7 @@
kind: Under the Hood
body: Put black config in explicit config
time: 2022-09-27T19:42:59.241433-07:00
custom:
Author: max-sixty
Issue: "5946"
PR: "5947"

View File

@@ -0,0 +1,7 @@
kind: Under the Hood
body: Added flat_graph attribute the Manifest class's deepcopy() coverage
time: 2022-09-29T13:44:06.275941-04:00
custom:
Author: peterallenwebb
Issue: "5809"
PR: "5975"

View File

@@ -0,0 +1,7 @@
kind: Under the Hood
body: Add mypy configs so `mypy` passes from CLI
time: 2022-10-05T12:03:10.061263-07:00
custom:
Author: max-sixty
Issue: "5983"
PR: "5983"

View File

@@ -0,0 +1,7 @@
kind: Under the Hood
body: Exception message cleanup.
time: 2022-10-07T09:46:27.682872-05:00
custom:
Author: emmyoop
Issue: "6023"
PR: "6024"

View File

@@ -0,0 +1,7 @@
kind: Under the Hood
body: Add dmypy cache to gitignore
time: 2022-10-07T14:00:44.227644-07:00
custom:
Author: max-sixty
Issue: "6028"
PR: "5978"

View File

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

View File

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

View File

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

View File

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

2
.gitattributes vendored
View File

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

20
.github/_README.md vendored
View File

@@ -63,12 +63,12 @@ permissions:
contents: read contents: read
pull-requests: write pull-requests: write
``` ```
### Secrets ### Secrets
- When to use a [Personal Access Token (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) vs the [GITHUB_TOKEN](https://docs.github.com/en/actions/security-guides/automatic-token-authentication) generated for the action? - When to use a [Personal Access Token (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) vs the [GITHUB_TOKEN](https://docs.github.com/en/actions/security-guides/automatic-token-authentication) generated for the action?
The `GITHUB_TOKEN` is used by default. In most cases it is sufficient for what you need. The `GITHUB_TOKEN` is used by default. In most cases it is sufficient for what you need.
If you expect the workflow to result in a commit to that should retrigger workflows, you will need to use a Personal Access Token for the bot to commit the file. When using the GITHUB_TOKEN, the resulting commit will not trigger another GitHub Actions Workflow run. This is due to limitations set by GitHub. See [the docs](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) for a more detailed explanation. If you expect the workflow to result in a commit to that should retrigger workflows, you will need to use a Personal Access Token for the bot to commit the file. When using the GITHUB_TOKEN, the resulting commit will not trigger another GitHub Actions Workflow run. This is due to limitations set by GitHub. See [the docs](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) for a more detailed explanation.
For example, we must use a PAT in our workflow to commit a new changelog yaml file for bot PRs. Once the file has been committed to the branch, it should retrigger the check to validate that a changelog exists on the PR. Otherwise, it would stay in a failed state since the check would never retrigger. For example, we must use a PAT in our workflow to commit a new changelog yaml file for bot PRs. Once the file has been committed to the branch, it should retrigger the check to validate that a changelog exists on the PR. Otherwise, it would stay in a failed state since the check would never retrigger.
@@ -105,7 +105,7 @@ Some triggers of note that we use:
``` ```
# **what?** # **what?**
# Describe what the action does. # Describe what the action does.
# **why?** # **why?**
# Why does this action exist? # Why does this action exist?
@@ -138,7 +138,7 @@ Some triggers of note that we use:
id: fp id: fp
run: | run: |
FILEPATH=.changes/unreleased/Dependencies-${{ steps.filename_time.outputs.time }}.yaml FILEPATH=.changes/unreleased/Dependencies-${{ steps.filename_time.outputs.time }}.yaml
echo "FILEPATH=$FILEPATH" >> $GITHUB_OUTPUT echo "::set-output name=FILEPATH::$FILEPATH"
``` ```
- Print out all variables you will reference as the first step of a job. This allows for easier debugging. The first job should log all inputs. Subsequent jobs should reference outputs of other jobs, if present. - Print out all variables you will reference as the first step of a job. This allows for easier debugging. The first job should log all inputs. Subsequent jobs should reference outputs of other jobs, if present.
@@ -158,14 +158,14 @@ Some triggers of note that we use:
echo "The build_script_path: ${{ inputs.build_script_path }}" echo "The build_script_path: ${{ inputs.build_script_path }}"
echo "The s3_bucket_name: ${{ inputs.s3_bucket_name }}" echo "The s3_bucket_name: ${{ inputs.s3_bucket_name }}"
echo "The package_test_command: ${{ inputs.package_test_command }}" echo "The package_test_command: ${{ inputs.package_test_command }}"
# collect all the variables that need to be used in subsequent jobs # collect all the variables that need to be used in subsequent jobs
- name: Set Variables - name: Set Variables
id: variables id: variables
run: | run: |
echo "important_path='performance/runner/Cargo.toml'" >> $GITHUB_OUTPUT echo "::set-output name=important_path::'performance/runner/Cargo.toml'"
echo "release_id=${{github.event.inputs.release_id}}" >> $GITHUB_OUTPUT echo "::set-output name=release_id::${{github.event.inputs.release_id}}"
echo "open_prs=${{github.event.inputs.open_prs}}" >> $GITHUB_OUTPUT echo "::set-output name=open_prs::${{github.event.inputs.open_prs}}"
job2: job2:
needs: [job1] needs: [job1]
@@ -190,7 +190,7 @@ ___
### Actions from the Marketplace ### Actions from the Marketplace
- Dont use external actions for things that can easily be accomplished manually. - Dont use external actions for things that can easily be accomplished manually.
- Always read through what an external action does before using it! Often an action in the GitHub Actions Marketplace can be replaced with a few lines in bash. This is much more maintainable (and wont change under us) and clear as to whats actually happening. It also prevents any - Always read through what an external action does before using it! Often an action in the GitHub Actions Marketplace can be replaced with a few lines in bash. This is much more maintainable (and wont change under us) and clear as to whats actually happening. It also prevents any
- Pin actions _we don't control_ to tags. - Pin actions _we don't control_ to tags.
### Connecting to AWS ### Connecting to AWS
- Authenticate with the aws managed workflow - Authenticate with the aws managed workflow
@@ -208,7 +208,7 @@ ___
```yaml ```yaml
- name: Copy Artifacts from S3 via CLI - name: Copy Artifacts from S3 via CLI
run: aws s3 cp ${{ env.s3_bucket }} . --recursive run: aws s3 cp ${{ env.s3_bucket }} . --recursive
``` ```
### Testing ### Testing

View File

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

View File

@@ -40,7 +40,7 @@ jobs:
matrix: matrix:
include: include:
- label: "dependencies" - label: "dependencies"
changie_kind: "Dependencies" changie_kind: "Dependency"
- label: "snyk" - label: "snyk"
changie_kind: "Security" changie_kind: "Security"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -58,4 +58,4 @@ jobs:
commit_message: "Add automated changelog yaml from template for bot PR" commit_message: "Add automated changelog yaml from template for bot PR"
changie_kind: ${{ matrix.changie_kind }} changie_kind: ${{ matrix.changie_kind }}
label: ${{ matrix.label }} label: ${{ matrix.label }}
custom_changelog_string: "custom:\n Author: ${{ github.event.pull_request.user.login }}\n PR: ${{ github.event.pull_request.number }}" custom_changelog_string: "custom:\n Author: ${{ github.event.pull_request.user.login }}\n Issue: 4904\n PR: ${{ github.event.pull_request.number }}"

View File

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

View File

@@ -45,9 +45,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Install python dependencies - name: Install python dependencies
run: | run: |
@@ -73,7 +71,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] python-version: ["3.7", "3.8", "3.9", "3.10"]
env: env:
TOXENV: "unit" TOXENV: "unit"
@@ -84,7 +82,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@@ -101,9 +99,7 @@ jobs:
- name: Get current date - name: Get current date
if: always() if: always()
id: date id: date
run: | run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" #no colons allowed for artifacts
CURRENT_DATE=$(date +'%Y-%m-%dT%H_%M_%S') # no colons allowed for artifacts
echo "date=$CURRENT_DATE" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: always() if: always()
@@ -120,8 +116,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] python-version: ["3.7", "3.8", "3.9", "3.10"]
os: [ubuntu-20.04] os: [ubuntu-latest]
include: include:
- python-version: 3.8 - python-version: 3.8
os: windows-latest os: windows-latest
@@ -141,7 +137,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@@ -170,9 +166,7 @@ jobs:
- name: Get current date - name: Get current date
if: always() if: always()
id: date id: date
run: | run: echo "::set-output name=date::$(date +'%Y_%m_%dT%H_%M_%S')" #no colons allowed for artifacts
CURRENT_DATE=$(date +'%Y-%m-%dT%H_%M_%S') # no colons allowed for artifacts
echo "date=$CURRENT_DATE" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: always() if: always()
@@ -196,9 +190,9 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2
with: with:
python-version: '3.8' python-version: 3.8
- name: Install python dependencies - name: Install python dependencies
run: | run: |

View File

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

View File

@@ -165,7 +165,7 @@ jobs:
env: env:
IS_PRERELEASE: ${{ contains(github.event.inputs.version_number, 'rc') || contains(github.event.inputs.version_number, 'b') }} IS_PRERELEASE: ${{ contains(github.event.inputs.version_number, 'rc') || contains(github.event.inputs.version_number, 'b') }}
run: | run: |
echo "isPrerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT echo ::set-output name=isPrerelease::$IS_PRERELEASE
- name: Creating GitHub Release - name: Creating GitHub Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1

View File

@@ -9,4 +9,13 @@ permissions:
jobs: jobs:
stale: stale:
uses: dbt-labs/actions/.github/workflows/stale-bot-matrix.yml@main runs-on: ubuntu-latest
steps:
# pinned at v4 (https://github.com/actions/stale/releases/tag/v4.0.0)
- uses: actions/stale@cdf15f641adb27a71842045a94023bef6945e3aa
with:
stale-issue-message: "This issue has been marked as Stale because it has been open for 180 days with no activity. If you would like the issue to remain open, please remove the stale label or comment on the issue, or it will be closed in 7 days."
stale-pr-message: "This PR has been marked as Stale because it has been open for 180 days with no activity. If you would like the PR to remain open, please remove the stale label or comment on the PR, or it will be closed in 7 days."
close-issue-message: "Although we are closing this issue as stale, it's not gone forever. Issues can be reopened if there is renewed community interest; add a comment to notify the maintainers."
# mark issues/PRs stale when they haven't seen activity in 180 days
days-before-stale: 180

View File

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

View File

@@ -65,7 +65,7 @@ jobs:
- name: Set branch value - name: Set branch value
id: variables id: variables
run: | run: |
echo "BRANCH_NAME=prep-release/${{ github.event.inputs.version_number }}_$GITHUB_RUN_ID" >> $GITHUB_OUTPUT echo "::set-output name=BRANCH_NAME::prep-release/${{ github.event.inputs.version_number }}_$GITHUB_RUN_ID"
- name: Create PR branch - name: Create PR branch
run: | run: |

1
.gitignore vendored
View File

@@ -11,7 +11,6 @@ __pycache__/
env*/ env*/
dbt_env/ dbt_env/
build/ build/
!core/dbt/docs/build
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/

View File

@@ -2,7 +2,7 @@
# Eventually the hooks described here will be run as tests before merging each PR. # Eventually the hooks described here will be run as tests before merging each PR.
# TODO: remove global exclusion of tests when testing overhaul is complete # TODO: remove global exclusion of tests when testing overhaul is complete
exclude: ^(test/|core/dbt/docs/build/) exclude: ^test/
# Force all unspecified python hooks to run python 3.8 # Force all unspecified python hooks to run python 3.8
default_language_version: default_language_version:
@@ -30,7 +30,7 @@ repos:
args: args:
- "--check" - "--check"
- "--diff" - "--diff"
- repo: https://github.com/pycqa/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: 4.0.1 rev: 4.0.1
hooks: hooks:
- id: flake8 - id: flake8

View File

@@ -5,12 +5,12 @@
- "Breaking changes" listed under a version may require action from end users or external maintainers when upgrading to that version. - "Breaking changes" listed under a version may require action from end users or external maintainers when upgrading to that version.
- Do not edit this file directly. This file is auto-generated using [changie](https://github.com/miniscruff/changie). For details on how to document a change, see [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#adding-changelog-entry) - Do not edit this file directly. This file is auto-generated using [changie](https://github.com/miniscruff/changie). For details on how to document a change, see [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#adding-changelog-entry)
## Previous Releases ## Previous Releases
For information on prior major and minor releases, see their changelogs: For information on prior major and minor releases, see their changelogs:
* [1.4](https://github.com/dbt-labs/dbt-core/blob/1.4.latest/CHANGELOG.md)
* [1.3](https://github.com/dbt-labs/dbt-core/blob/1.3.latest/CHANGELOG.md) * [1.3](https://github.com/dbt-labs/dbt-core/blob/1.3.latest/CHANGELOG.md)
* [1.2](https://github.com/dbt-labs/dbt-core/blob/1.2.latest/CHANGELOG.md) * [1.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.1](https://github.com/dbt-labs/dbt-core/blob/1.1.latest/CHANGELOG.md)

View File

@@ -56,7 +56,7 @@ There are some tools that will be helpful to you in developing locally. While th
These are the tools used in `dbt-core` development and testing: These are the tools used in `dbt-core` development and testing:
- [`tox`](https://tox.readthedocs.io/en/latest/) to manage virtualenvs across python versions. We currently target the latest patch releases for Python 3.7, 3.8, 3.9, 3.10 and 3.11 - [`tox`](https://tox.readthedocs.io/en/latest/) to manage virtualenvs across python versions. We currently target the latest patch releases for Python 3.7, 3.8, 3.9, and 3.10
- [`pytest`](https://docs.pytest.org/en/latest/) to define, discover, and run tests - [`pytest`](https://docs.pytest.org/en/latest/) to define, discover, and run tests
- [`flake8`](https://flake8.pycqa.org/en/latest/) for code linting - [`flake8`](https://flake8.pycqa.org/en/latest/) for code linting
- [`black`](https://github.com/psf/black) for code formatting - [`black`](https://github.com/psf/black) for code formatting
@@ -96,15 +96,12 @@ brew install postgresql
### Installation ### Installation
First make sure that you set up your `virtualenv` as described in [Setting up an environment](#setting-up-an-environment). Also ensure you have the latest version of pip installed with `pip install --upgrade pip`. Next, install `dbt-core` (and its dependencies): First make sure that you set up your `virtualenv` as described in [Setting up an environment](#setting-up-an-environment). Also ensure you have the latest version of pip installed with `pip install --upgrade pip`. Next, install `dbt-core` (and its dependencies) with:
```sh ```sh
make dev make dev
``` # or
or, alternatively:
```sh
pip install -r dev-requirements.txt -r editable-requirements.txt pip install -r dev-requirements.txt -r editable-requirements.txt
pre-commit install
``` ```
When installed in this way, any changes you make to your local copy of the source code will be reflected immediately in your next `dbt` run. When installed in this way, any changes you make to your local copy of the source code will be reflected immediately in your next `dbt` run.
@@ -163,7 +160,7 @@ suites.
#### `tox` #### `tox`
[`tox`](https://tox.readthedocs.io/en/latest/) takes care of managing virtualenvs and install dependencies in order to run tests. You can also run tests in parallel, for example, you can run unit tests for Python 3.7, Python 3.8, Python 3.9, Python 3.10 and Python 3.11 checks in parallel with `tox -p`. Also, you can run unit tests for specific python versions with `tox -e py37`. The configuration for these tests in located in `tox.ini`. [`tox`](https://tox.readthedocs.io/en/latest/) takes care of managing virtualenvs and install dependencies in order to run tests. You can also run tests in parallel, for example, you can run unit tests for Python 3.7, Python 3.8, Python 3.9, and Python 3.10 checks in parallel with `tox -p`. Also, you can run unit tests for specific python versions with `tox -e py37`. The configuration for these tests in located in `tox.ini`.
#### `pytest` #### `pytest`
@@ -204,21 +201,13 @@ Here are some general rules for adding tests:
* Sometimes flake8 complains about lines that are actually fine, in which case you can put a comment on the line such as: # noqa or # noqa: ANNN, where ANNN is the error code that flake8 issues. * Sometimes flake8 complains about lines that are actually fine, in which case you can put a comment on the line such as: # noqa or # noqa: ANNN, where ANNN is the error code that flake8 issues.
* To collect output for `CProfile`, run dbt with the `-r` option and the name of an output file, i.e. `dbt -r dbt.cprof run`. If you just want to profile parsing, you can do: `dbt -r dbt.cprof parse`. `pip` install `snakeviz` to view the output. Run `snakeviz dbt.cprof` and output will be rendered in a browser window. * To collect output for `CProfile`, run dbt with the `-r` option and the name of an output file, i.e. `dbt -r dbt.cprof run`. If you just want to profile parsing, you can do: `dbt -r dbt.cprof parse`. `pip` install `snakeviz` to view the output. Run `snakeviz dbt.cprof` and output will be rendered in a browser window.
## Adding or modifying a CHANGELOG Entry ## Adding a CHANGELOG Entry
We use [changie](https://changie.dev) to generate `CHANGELOG` entries. **Note:** Do not edit the `CHANGELOG.md` directly. Your modifications will be lost. We use [changie](https://changie.dev) to generate `CHANGELOG` entries. **Note:** Do not edit the `CHANGELOG.md` directly. Your modifications will be lost.
Follow the steps to [install `changie`](https://changie.dev/guide/installation/) for your system. Follow the steps to [install `changie`](https://changie.dev/guide/installation/) for your system.
Once changie is installed and your PR is created for a new feature, simply run the following command and changie will walk you through the process of creating a changelog entry: Once changie is installed and your PR is created, simply run `changie new` and changie will walk you through the process of creating a changelog entry. Commit the file that's created and your changelog entry is complete!
```shell
changie new
```
Commit the file that's created and your changelog entry is complete!
If you are contributing to a feature already in progress, you will modify the changie yaml file in dbt/.changes/unreleased/ related to your change. If you need help finding this file, please ask within the discussion for the pull request!
You don't need to worry about which `dbt-core` version your change will go into. Just create the changelog entry with `changie`, and open your PR against the `main` branch. All merged changes will be included in the next minor version of `dbt-core`. The Core maintainers _may_ choose to "backport" specific changes in order to patch older minor versions. In that case, a maintainer will take care of that backport after merging your PR, before releasing the new version of `dbt-core`. You don't need to worry about which `dbt-core` version your change will go into. Just create the changelog entry with `changie`, and open your PR against the `main` branch. All merged changes will be included in the next minor version of `dbt-core`. The Core maintainers _may_ choose to "backport" specific changes in order to patch older minor versions. In that case, a maintainer will take care of that backport after merging your PR, before releasing the new version of `dbt-core`.

View File

@@ -49,9 +49,6 @@ RUN apt-get update \
python3.10 \ python3.10 \
python3.10-dev \ python3.10-dev \
python3.10-venv \ python3.10-venv \
python3.11 \
python3.11-dev \
python3.11-venv \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

View File

@@ -19,16 +19,11 @@ CI_FLAGS =\
LOG_DIR=./logs\ LOG_DIR=./logs\
DBT_LOG_FORMAT=json DBT_LOG_FORMAT=json
.PHONY: dev_req .PHONY: dev
dev_req: ## Installs dbt-* packages in develop mode along with only development dependencies. dev: ## Installs dbt-* packages in develop mode along with development dependencies.
@\ @\
pip install -r dev-requirements.txt -r editable-requirements.txt pip install -r dev-requirements.txt -r editable-requirements.txt
.PHONY: dev
dev: dev_req ## Installs dbt-* packages in develop mode along with development dependencies and pre-commit.
@\
pre-commit install
.PHONY: mypy .PHONY: mypy
mypy: .env ## Runs mypy against staged changes for static type checking. mypy: .env ## Runs mypy against staged changes for static type checking.
@\ @\

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ import abc
import os import os
from time import sleep from time import sleep
import sys import sys
import traceback
# multiprocessing.RLock is a function returning this type # multiprocessing.RLock is a function returning this type
from multiprocessing.synchronize import RLock from multiprocessing.synchronize import RLock
@@ -41,16 +40,14 @@ from dbt.events.functions import fire_event
from dbt.events.types import ( from dbt.events.types import (
NewConnection, NewConnection,
ConnectionReused, ConnectionReused,
ConnectionLeftOpenInCleanup,
ConnectionLeftOpen, ConnectionLeftOpen,
ConnectionClosedInCleanup, ConnectionLeftOpen2,
ConnectionClosed, ConnectionClosed,
ConnectionClosed2,
Rollback, Rollback,
RollbackFailed, RollbackFailed,
) )
from dbt.events.contextvars import get_node_info
from dbt import flags from dbt import flags
from dbt.utils import cast_to_str
SleepTime = Union[int, float] # As taken by time.sleep. SleepTime = Union[int, float] # As taken by time.sleep.
AdapterHandle = Any # Adapter connection handle objects can be any class. AdapterHandle = Any # Adapter connection handle objects can be any class.
@@ -91,13 +88,13 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
key = self.get_thread_identifier() key = self.get_thread_identifier()
with self.lock: with self.lock:
if key not in self.thread_connections: if key not in self.thread_connections:
raise dbt.exceptions.InvalidConnectionError(key, list(self.thread_connections)) raise dbt.exceptions.InvalidConnectionException(key, list(self.thread_connections))
return self.thread_connections[key] return self.thread_connections[key]
def set_thread_connection(self, conn: Connection) -> None: def set_thread_connection(self, conn: Connection) -> None:
key = self.get_thread_identifier() key = self.get_thread_identifier()
if key in self.thread_connections: if key in self.thread_connections:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
"In set_thread_connection, existing connection exists for {}" "In set_thread_connection, existing connection exists for {}"
) )
self.thread_connections[key] = conn self.thread_connections[key] = conn
@@ -137,49 +134,47 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
:return: A context manager that handles exceptions raised by the :return: A context manager that handles exceptions raised by the
underlying database. underlying database.
""" """
raise dbt.exceptions.NotImplementedError( raise dbt.exceptions.NotImplementedException(
"`exception_handler` is not implemented for this adapter!" "`exception_handler` is not implemented for this adapter!"
) )
def set_connection_name(self, name: Optional[str] = None) -> Connection: def set_connection_name(self, name: Optional[str] = None) -> Connection:
"""Called by 'acquire_connection' in BaseAdapter, which is called by conn_name: str
'connection_named', called by 'connection_for(node)'. if name is None:
Creates a connection for this thread if one doesn't already # if a name isn't specified, we'll re-use a single handle
exist, and will rename an existing connection.""" # named 'master'
conn_name = "master"
else:
if not isinstance(name, str):
raise dbt.exceptions.CompilerException(
f"For connection name, got {name} - not a string!"
)
assert isinstance(name, str)
conn_name = name
conn_name: str = "master" if name is None else name
# Get a connection for this thread
conn = self.get_if_exists() conn = self.get_if_exists()
if conn and conn.name == conn_name and conn.state == "open":
# Found a connection and nothing to do, so just return it
return conn
if conn is None: if conn is None:
# Create a new connection
conn = Connection( conn = Connection(
type=Identifier(self.TYPE), type=Identifier(self.TYPE),
name=conn_name, name=None,
state=ConnectionState.INIT, state=ConnectionState.INIT,
transaction_open=False, transaction_open=False,
handle=None, handle=None,
credentials=self.profile.credentials, credentials=self.profile.credentials,
) )
conn.handle = LazyHandle(self.open)
# Add the connection to thread_connections for this thread
self.set_thread_connection(conn) self.set_thread_connection(conn)
fire_event(
NewConnection(conn_name=conn_name, conn_type=self.TYPE, node_info=get_node_info())
)
else: # existing connection either wasn't open or didn't have the right name
if conn.state != "open":
conn.handle = LazyHandle(self.open)
if conn.name != conn_name:
orig_conn_name: str = conn.name or ""
conn.name = conn_name
fire_event(ConnectionReused(orig_conn_name=orig_conn_name, conn_name=conn_name))
if conn.name == conn_name and conn.state == "open":
return conn
fire_event(NewConnection(conn_name=conn_name, conn_type=self.TYPE))
if conn.state == "open":
fire_event(ConnectionReused(conn_name=conn_name))
else:
conn.handle = LazyHandle(self.open)
conn.name = conn_name
return conn return conn
@classmethod @classmethod
@@ -211,7 +206,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
connect should trigger a retry. connect should trigger a retry.
:type retryable_exceptions: Iterable[Type[Exception]] :type retryable_exceptions: Iterable[Type[Exception]]
:param int retry_limit: How many times to retry the call to connect. If this limit :param int retry_limit: How many times to retry the call to connect. If this limit
is exceeded before a successful call, a FailedToConnectError will be raised. is exceeded before a successful call, a FailedToConnectException will be raised.
Must be non-negative. Must be non-negative.
:param retry_timeout: Time to wait between attempts to connect. Can also take a :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 Callable that takes the number of attempts so far, beginning at 0, and returns an int
@@ -220,14 +215,14 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
:param int _attempts: Parameter used to keep track of the number of attempts in calling the :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 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. is a Callable. This parameter should not be set by the initial caller.
:raises dbt.exceptions.FailedToConnectError: Upon exhausting all retry attempts without :raises dbt.exceptions.FailedToConnectException: Upon exhausting all retry attempts without
successfully acquiring a handle. successfully acquiring a handle.
:return: The given connection with its appropriate state and handle attributes set :return: The given connection with its appropriate state and handle attributes set
depending on whether we successfully acquired a handle or not. depending on whether we successfully acquired a handle or not.
""" """
timeout = retry_timeout(_attempts) if callable(retry_timeout) else retry_timeout timeout = retry_timeout(_attempts) if callable(retry_timeout) else retry_timeout
if timeout < 0: if timeout < 0:
raise dbt.exceptions.FailedToConnectError( raise dbt.exceptions.FailedToConnectException(
"retry_timeout cannot be negative or return a negative time." "retry_timeout cannot be negative or return a negative time."
) )
@@ -235,7 +230,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
# This guard is not perfect others may add to the recursion limit (e.g. built-ins). # This guard is not perfect others may add to the recursion limit (e.g. built-ins).
connection.handle = None connection.handle = None
connection.state = ConnectionState.FAIL connection.state = ConnectionState.FAIL
raise dbt.exceptions.FailedToConnectError("retry_limit cannot be negative") raise dbt.exceptions.FailedToConnectException("retry_limit cannot be negative")
try: try:
connection.handle = connect() connection.handle = connect()
@@ -246,7 +241,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
if retry_limit <= 0: if retry_limit <= 0:
connection.handle = None connection.handle = None
connection.state = ConnectionState.FAIL connection.state = ConnectionState.FAIL
raise dbt.exceptions.FailedToConnectError(str(e)) raise dbt.exceptions.FailedToConnectException(str(e))
logger.debug( logger.debug(
f"Got a retryable error when attempting to open a {cls.TYPE} connection.\n" f"Got a retryable error when attempting to open a {cls.TYPE} connection.\n"
@@ -268,12 +263,12 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
except Exception as e: except Exception as e:
connection.handle = None connection.handle = None
connection.state = ConnectionState.FAIL connection.state = ConnectionState.FAIL
raise dbt.exceptions.FailedToConnectError(str(e)) raise dbt.exceptions.FailedToConnectException(str(e))
@abc.abstractmethod @abc.abstractmethod
def cancel_open(self) -> Optional[List[str]]: def cancel_open(self) -> Optional[List[str]]:
"""Cancel all open connections on the adapter. (passable)""" """Cancel all open connections on the adapter. (passable)"""
raise dbt.exceptions.NotImplementedError( raise dbt.exceptions.NotImplementedException(
"`cancel_open` is not implemented for this adapter!" "`cancel_open` is not implemented for this adapter!"
) )
@@ -288,7 +283,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
This should be thread-safe, or hold the lock if necessary. The given This should be thread-safe, or hold the lock if necessary. The given
connection should not be in either in_use or available. connection should not be in either in_use or available.
""" """
raise dbt.exceptions.NotImplementedError("`open` is not implemented for this adapter!") raise dbt.exceptions.NotImplementedException("`open` is not implemented for this adapter!")
def release(self) -> None: def release(self) -> None:
with self.lock: with self.lock:
@@ -309,9 +304,9 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
with self.lock: with self.lock:
for connection in self.thread_connections.values(): for connection in self.thread_connections.values():
if connection.state not in {"closed", "init"}: if connection.state not in {"closed", "init"}:
fire_event(ConnectionLeftOpenInCleanup(conn_name=cast_to_str(connection.name))) fire_event(ConnectionLeftOpen(conn_name=connection.name))
else: else:
fire_event(ConnectionClosedInCleanup(conn_name=cast_to_str(connection.name))) fire_event(ConnectionClosed(conn_name=connection.name))
self.close(connection) self.close(connection)
# garbage collect these connections # garbage collect these connections
@@ -320,12 +315,16 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
@abc.abstractmethod @abc.abstractmethod
def begin(self) -> None: def begin(self) -> None:
"""Begin a transaction. (passable)""" """Begin a transaction. (passable)"""
raise dbt.exceptions.NotImplementedError("`begin` is not implemented for this adapter!") raise dbt.exceptions.NotImplementedException(
"`begin` is not implemented for this adapter!"
)
@abc.abstractmethod @abc.abstractmethod
def commit(self) -> None: def commit(self) -> None:
"""Commit a transaction. (passable)""" """Commit a transaction. (passable)"""
raise dbt.exceptions.NotImplementedError("`commit` is not implemented for this adapter!") raise dbt.exceptions.NotImplementedException(
"`commit` is not implemented for this adapter!"
)
@classmethod @classmethod
def _rollback_handle(cls, connection: Connection) -> None: def _rollback_handle(cls, connection: Connection) -> None:
@@ -333,40 +332,28 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
try: try:
connection.handle.rollback() connection.handle.rollback()
except Exception: except Exception:
fire_event( fire_event(RollbackFailed(conn_name=connection.name))
RollbackFailed(
conn_name=cast_to_str(connection.name),
exc_info=traceback.format_exc(),
node_info=get_node_info(),
)
)
@classmethod @classmethod
def _close_handle(cls, connection: Connection) -> None: def _close_handle(cls, connection: Connection) -> None:
"""Perform the actual close operation.""" """Perform the actual close operation."""
# On windows, sometimes connection handles don't have a close() attr. # On windows, sometimes connection handles don't have a close() attr.
if hasattr(connection.handle, "close"): if hasattr(connection.handle, "close"):
fire_event( fire_event(ConnectionClosed2(conn_name=connection.name))
ConnectionClosed(conn_name=cast_to_str(connection.name), node_info=get_node_info())
)
connection.handle.close() connection.handle.close()
else: else:
fire_event( fire_event(ConnectionLeftOpen2(conn_name=connection.name))
ConnectionLeftOpen(
conn_name=cast_to_str(connection.name), node_info=get_node_info()
)
)
@classmethod @classmethod
def _rollback(cls, connection: Connection) -> None: def _rollback(cls, connection: Connection) -> None:
"""Roll back the given connection.""" """Roll back the given connection."""
if connection.transaction_open is False: if connection.transaction_open is False:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
f"Tried to rollback transaction on connection " f"Tried to rollback transaction on connection "
f'"{connection.name}", but it does not have one open!' f'"{connection.name}", but it does not have one open!'
) )
fire_event(Rollback(conn_name=cast_to_str(connection.name), node_info=get_node_info())) fire_event(Rollback(conn_name=connection.name))
cls._rollback_handle(connection) cls._rollback_handle(connection)
connection.transaction_open = False connection.transaction_open = False
@@ -378,7 +365,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
return connection return connection
if connection.transaction_open and connection.handle: if connection.transaction_open and connection.handle:
fire_event(Rollback(conn_name=cast_to_str(connection.name), node_info=get_node_info())) fire_event(Rollback(conn_name=connection.name))
cls._rollback_handle(connection) cls._rollback_handle(connection)
connection.transaction_open = False connection.transaction_open = False
@@ -411,4 +398,6 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
:return: A tuple of the query status and results (empty if fetch=False). :return: A tuple of the query status and results (empty if fetch=False).
:rtype: Tuple[AdapterResponse, agate.Table] :rtype: Tuple[AdapterResponse, agate.Table]
""" """
raise dbt.exceptions.NotImplementedError("`execute` is not implemented for this adapter!") raise dbt.exceptions.NotImplementedException(
"`execute` is not implemented for this adapter!"
)

View File

@@ -15,6 +15,7 @@ from typing import (
List, List,
Mapping, Mapping,
Iterator, Iterator,
Union,
Set, Set,
) )
@@ -22,20 +23,13 @@ import agate
import pytz import pytz
from dbt.exceptions import ( from dbt.exceptions import (
DbtInternalError, raise_database_error,
MacroArgTypeError, raise_compiler_error,
MacroResultError, invalid_type_error,
QuoteConfigTypeError, get_relation_returned_multiple_results,
NotImplementedError, InternalException,
NullRelationCacheAttemptedError, NotImplementedException,
NullRelationDropAttemptedError, RuntimeException,
RelationReturnedMultipleResultsError,
RenameToNoneAttemptedError,
DbtRuntimeError,
SnapshotTargetIncompleteError,
SnapshotTargetNotSnapshotTableError,
UnexpectedNullError,
UnexpectedNonTimestampError,
) )
from dbt.adapters.protocol import ( from dbt.adapters.protocol import (
@@ -44,17 +38,18 @@ from dbt.adapters.protocol import (
) )
from dbt.clients.agate_helper import empty_table, merge_tables, table_from_rows from dbt.clients.agate_helper import empty_table, merge_tables, table_from_rows
from dbt.clients.jinja import MacroGenerator from dbt.clients.jinja import MacroGenerator
from dbt.contracts.graph.compiled import CompileResultNode, CompiledSeedNode
from dbt.contracts.graph.manifest import Manifest, MacroManifest from dbt.contracts.graph.manifest import Manifest, MacroManifest
from dbt.contracts.graph.nodes import ResultNode from dbt.contracts.graph.parsed import ParsedSeedNode
from dbt.events.functions import fire_event, warn_or_error from dbt.exceptions import warn_or_error
from dbt.events.functions import fire_event
from dbt.events.types import ( from dbt.events.types import (
CacheMiss, CacheMiss,
ListRelations, ListRelations,
CodeExecution, CodeExecution,
CodeExecutionStatus, CodeExecutionStatus,
CatalogGenerationError,
) )
from dbt.utils import filter_null_values, executor, cast_to_str from dbt.utils import filter_null_values, executor
from dbt.adapters.base.connections import Connection, AdapterResponse from dbt.adapters.base.connections import Connection, AdapterResponse
from dbt.adapters.base.meta import AdapterMeta, available from dbt.adapters.base.meta import AdapterMeta, available
@@ -66,7 +61,10 @@ from dbt.adapters.base.relation import (
) )
from dbt.adapters.base import Column as BaseColumn from dbt.adapters.base import Column as BaseColumn
from dbt.adapters.base import Credentials from dbt.adapters.base import Credentials
from dbt.adapters.cache import RelationsCache, _make_ref_key_msg from dbt.adapters.cache import RelationsCache, _make_key
SeedModel = Union[ParsedSeedNode, CompiledSeedNode]
GET_CATALOG_MACRO_NAME = "get_catalog" GET_CATALOG_MACRO_NAME = "get_catalog"
@@ -75,7 +73,7 @@ FRESHNESS_MACRO_NAME = "collect_freshness"
def _expect_row_value(key: str, row: agate.Row): def _expect_row_value(key: str, row: agate.Row):
if key not in row.keys(): if key not in row.keys():
raise DbtInternalError( raise InternalException(
'Got a row without "{}" column, columns: {}'.format(key, row.keys()) 'Got a row without "{}" column, columns: {}'.format(key, row.keys())
) )
return row[key] return row[key]
@@ -104,10 +102,18 @@ def _utc(dt: Optional[datetime], source: BaseRelation, field_name: str) -> datet
assume the datetime is already for UTC and add the timezone. assume the datetime is already for UTC and add the timezone.
""" """
if dt is None: if dt is None:
raise UnexpectedNullError(field_name, source) raise raise_database_error(
"Expected a non-null value when querying field '{}' of table "
" {} but received value 'null' instead".format(field_name, source)
)
elif not hasattr(dt, "tzinfo"): elif not hasattr(dt, "tzinfo"):
raise UnexpectedNonTimestampError(field_name, source, dt) raise raise_database_error(
"Expected a timestamp value when querying field '{}' of table "
"{} but received value of type '{}' instead".format(
field_name, source, type(dt).__name__
)
)
elif dt.tzinfo: elif dt.tzinfo:
return dt.astimezone(pytz.UTC) return dt.astimezone(pytz.UTC)
@@ -237,7 +243,9 @@ class BaseAdapter(metaclass=AdapterMeta):
return conn.name return conn.name
@contextmanager @contextmanager
def connection_named(self, name: str, node: Optional[ResultNode] = None) -> Iterator[None]: def connection_named(
self, name: str, node: Optional[CompileResultNode] = None
) -> Iterator[None]:
try: try:
if self.connections.query_header is not None: if self.connections.query_header is not None:
self.connections.query_header.set(name, node) self.connections.query_header.set(name, node)
@@ -249,7 +257,7 @@ class BaseAdapter(metaclass=AdapterMeta):
self.connections.query_header.reset() self.connections.query_header.reset()
@contextmanager @contextmanager
def connection_for(self, node: ResultNode) -> Iterator[None]: def connection_for(self, node: CompileResultNode) -> Iterator[None]:
with self.connection_named(node.unique_id, node): with self.connection_named(node.unique_id, node):
yield yield
@@ -335,7 +343,7 @@ class BaseAdapter(metaclass=AdapterMeta):
fire_event( fire_event(
CacheMiss( CacheMiss(
conn_name=self.nice_connection_name(), conn_name=self.nice_connection_name(),
database=cast_to_str(database), database=database,
schema=schema, schema=schema,
) )
) )
@@ -364,7 +372,7 @@ class BaseAdapter(metaclass=AdapterMeta):
lowercase strings. lowercase strings.
""" """
info_schema_name_map = SchemaSearchMap() info_schema_name_map = SchemaSearchMap()
nodes: Iterator[ResultNode] = chain( nodes: Iterator[CompileResultNode] = chain(
[ [
node node
for node in manifest.nodes.values() for node in manifest.nodes.values()
@@ -433,7 +441,7 @@ class BaseAdapter(metaclass=AdapterMeta):
"""Cache a new relation in dbt. It will show up in `list relations`.""" """Cache a new relation in dbt. It will show up in `list relations`."""
if relation is None: if relation is None:
name = self.nice_connection_name() name = self.nice_connection_name()
raise NullRelationCacheAttemptedError(name) raise_compiler_error("Attempted to cache a null relation for {}".format(name))
self.cache.add(relation) self.cache.add(relation)
# so jinja doesn't render things # so jinja doesn't render things
return "" return ""
@@ -445,7 +453,7 @@ class BaseAdapter(metaclass=AdapterMeta):
""" """
if relation is None: if relation is None:
name = self.nice_connection_name() name = self.nice_connection_name()
raise NullRelationDropAttemptedError(name) raise_compiler_error("Attempted to drop a null relation for {}".format(name))
self.cache.drop(relation) self.cache.drop(relation)
return "" return ""
@@ -462,7 +470,9 @@ class BaseAdapter(metaclass=AdapterMeta):
name = self.nice_connection_name() name = self.nice_connection_name()
src_name = _relation_name(from_relation) src_name = _relation_name(from_relation)
dst_name = _relation_name(to_relation) dst_name = _relation_name(to_relation)
raise RenameToNoneAttemptedError(src_name, dst_name, name) raise_compiler_error(
"Attempted to rename {} to {} for {}".format(src_name, dst_name, name)
)
self.cache.rename(from_relation, to_relation) self.cache.rename(from_relation, to_relation)
return "" return ""
@@ -474,12 +484,12 @@ class BaseAdapter(metaclass=AdapterMeta):
@abc.abstractmethod @abc.abstractmethod
def date_function(cls) -> str: def date_function(cls) -> str:
"""Get the date function used by this adapter's database.""" """Get the date function used by this adapter's database."""
raise NotImplementedError("`date_function` is not implemented for this adapter!") raise NotImplementedException("`date_function` is not implemented for this adapter!")
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
def is_cancelable(cls) -> bool: def is_cancelable(cls) -> bool:
raise NotImplementedError("`is_cancelable` is not implemented for this adapter!") raise NotImplementedException("`is_cancelable` is not implemented for this adapter!")
### ###
# Abstract methods about schemas # Abstract methods about schemas
@@ -487,7 +497,7 @@ class BaseAdapter(metaclass=AdapterMeta):
@abc.abstractmethod @abc.abstractmethod
def list_schemas(self, database: str) -> List[str]: def list_schemas(self, database: str) -> List[str]:
"""Get a list of existing schemas in database""" """Get a list of existing schemas in database"""
raise NotImplementedError("`list_schemas` is not implemented for this adapter!") raise NotImplementedException("`list_schemas` is not implemented for this adapter!")
@available.parse(lambda *a, **k: False) @available.parse(lambda *a, **k: False)
def check_schema_exists(self, database: str, schema: str) -> bool: def check_schema_exists(self, database: str, schema: str) -> bool:
@@ -510,13 +520,13 @@ class BaseAdapter(metaclass=AdapterMeta):
*Implementors must call self.cache.drop() to preserve cache state!* *Implementors must call self.cache.drop() to preserve cache state!*
""" """
raise NotImplementedError("`drop_relation` is not implemented for this adapter!") raise NotImplementedException("`drop_relation` is not implemented for this adapter!")
@abc.abstractmethod @abc.abstractmethod
@available.parse_none @available.parse_none
def truncate_relation(self, relation: BaseRelation) -> None: def truncate_relation(self, relation: BaseRelation) -> None:
"""Truncate the given relation.""" """Truncate the given relation."""
raise NotImplementedError("`truncate_relation` is not implemented for this adapter!") raise NotImplementedException("`truncate_relation` is not implemented for this adapter!")
@abc.abstractmethod @abc.abstractmethod
@available.parse_none @available.parse_none
@@ -525,13 +535,15 @@ class BaseAdapter(metaclass=AdapterMeta):
Implementors must call self.cache.rename() to preserve cache state. Implementors must call self.cache.rename() to preserve cache state.
""" """
raise NotImplementedError("`rename_relation` is not implemented for this adapter!") raise NotImplementedException("`rename_relation` is not implemented for this adapter!")
@abc.abstractmethod @abc.abstractmethod
@available.parse_list @available.parse_list
def get_columns_in_relation(self, relation: BaseRelation) -> List[BaseColumn]: def get_columns_in_relation(self, relation: BaseRelation) -> List[BaseColumn]:
"""Get a list of the columns in the given Relation.""" """Get a list of the columns in the given Relation."""
raise NotImplementedError("`get_columns_in_relation` is not implemented for this adapter!") raise NotImplementedException(
"`get_columns_in_relation` is not implemented for this adapter!"
)
@available.deprecated("get_columns_in_relation", lambda *a, **k: []) @available.deprecated("get_columns_in_relation", lambda *a, **k: [])
def get_columns_in_table(self, schema: str, identifier: str) -> List[BaseColumn]: def get_columns_in_table(self, schema: str, identifier: str) -> List[BaseColumn]:
@@ -553,7 +565,7 @@ class BaseAdapter(metaclass=AdapterMeta):
:param self.Relation current: A relation that currently exists in the :param self.Relation current: A relation that currently exists in the
database with columns of unspecified types. database with columns of unspecified types.
""" """
raise NotImplementedError( raise NotImplementedException(
"`expand_target_column_types` is not implemented for this adapter!" "`expand_target_column_types` is not implemented for this adapter!"
) )
@@ -568,8 +580,8 @@ class BaseAdapter(metaclass=AdapterMeta):
:return: The relations in schema :return: The relations in schema
:rtype: List[self.Relation] :rtype: List[self.Relation]
""" """
raise NotImplementedError( raise NotImplementedException(
"`list_relations_without_caching` is not implemented for this adapter!" "`list_relations_without_caching` is not implemented for this " "adapter!"
) )
### ###
@@ -610,7 +622,7 @@ class BaseAdapter(metaclass=AdapterMeta):
to_relation. to_relation.
""" """
if not isinstance(from_relation, self.Relation): if not isinstance(from_relation, self.Relation):
raise MacroArgTypeError( invalid_type_error(
method_name="get_missing_columns", method_name="get_missing_columns",
arg_name="from_relation", arg_name="from_relation",
got_value=from_relation, got_value=from_relation,
@@ -618,7 +630,7 @@ class BaseAdapter(metaclass=AdapterMeta):
) )
if not isinstance(to_relation, self.Relation): if not isinstance(to_relation, self.Relation):
raise MacroArgTypeError( invalid_type_error(
method_name="get_missing_columns", method_name="get_missing_columns",
arg_name="to_relation", arg_name="to_relation",
got_value=to_relation, got_value=to_relation,
@@ -639,11 +651,11 @@ class BaseAdapter(metaclass=AdapterMeta):
expected columns. expected columns.
:param Relation relation: The relation to check :param Relation relation: The relation to check
:raises InvalidMacroArgType: If the columns are :raises CompilationException: If the columns are
incorrect. incorrect.
""" """
if not isinstance(relation, self.Relation): if not isinstance(relation, self.Relation):
raise MacroArgTypeError( invalid_type_error(
method_name="valid_snapshot_target", method_name="valid_snapshot_target",
arg_name="relation", arg_name="relation",
got_value=relation, got_value=relation,
@@ -664,16 +676,24 @@ class BaseAdapter(metaclass=AdapterMeta):
if missing: if missing:
if extra: if extra:
raise SnapshotTargetIncompleteError(extra, missing) msg = (
'Snapshot target has ("{}") but not ("{}") - is it an '
"unmigrated previous version archive?".format(
'", "'.join(extra), '", "'.join(missing)
)
)
else: else:
raise SnapshotTargetNotSnapshotTableError(missing) msg = 'Snapshot target is not a snapshot table (missing "{}")'.format(
'", "'.join(missing)
)
raise_compiler_error(msg)
@available.parse_none @available.parse_none
def expand_target_column_types( def expand_target_column_types(
self, from_relation: BaseRelation, to_relation: BaseRelation self, from_relation: BaseRelation, to_relation: BaseRelation
) -> None: ) -> None:
if not isinstance(from_relation, self.Relation): if not isinstance(from_relation, self.Relation):
raise MacroArgTypeError( invalid_type_error(
method_name="expand_target_column_types", method_name="expand_target_column_types",
arg_name="from_relation", arg_name="from_relation",
got_value=from_relation, got_value=from_relation,
@@ -681,7 +701,7 @@ class BaseAdapter(metaclass=AdapterMeta):
) )
if not isinstance(to_relation, self.Relation): if not isinstance(to_relation, self.Relation):
raise MacroArgTypeError( invalid_type_error(
method_name="expand_target_column_types", method_name="expand_target_column_types",
arg_name="to_relation", arg_name="to_relation",
got_value=to_relation, got_value=to_relation,
@@ -706,9 +726,9 @@ class BaseAdapter(metaclass=AdapterMeta):
relations = self.list_relations_without_caching(schema_relation) relations = self.list_relations_without_caching(schema_relation)
fire_event( fire_event(
ListRelations( ListRelations(
database=cast_to_str(database), database=database,
schema=schema, schema=schema,
relations=[_make_ref_key_msg(x) for x in relations], relations=[_make_key(x) for x in relations],
) )
) )
@@ -763,7 +783,7 @@ class BaseAdapter(metaclass=AdapterMeta):
"schema": schema, "schema": schema,
"database": database, "database": database,
} }
raise RelationReturnedMultipleResultsError(kwargs, matches) get_relation_returned_multiple_results(kwargs, matches)
elif matches: elif matches:
return matches[0] return matches[0]
@@ -785,20 +805,20 @@ class BaseAdapter(metaclass=AdapterMeta):
@available.parse_none @available.parse_none
def create_schema(self, relation: BaseRelation): def create_schema(self, relation: BaseRelation):
"""Create the given schema if it does not exist.""" """Create the given schema if it does not exist."""
raise NotImplementedError("`create_schema` is not implemented for this adapter!") raise NotImplementedException("`create_schema` is not implemented for this adapter!")
@abc.abstractmethod @abc.abstractmethod
@available.parse_none @available.parse_none
def drop_schema(self, relation: BaseRelation): def drop_schema(self, relation: BaseRelation):
"""Drop the given schema (and everything in it) if it exists.""" """Drop the given schema (and everything in it) if it exists."""
raise NotImplementedError("`drop_schema` is not implemented for this adapter!") raise NotImplementedException("`drop_schema` is not implemented for this adapter!")
@available @available
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
def quote(cls, identifier: str) -> str: def quote(cls, identifier: str) -> str:
"""Quote the given identifier, as appropriate for the database.""" """Quote the given identifier, as appropriate for the database."""
raise NotImplementedError("`quote` is not implemented for this adapter!") raise NotImplementedException("`quote` is not implemented for this adapter!")
@available @available
def quote_as_configured(self, identifier: str, quote_key: str) -> str: def quote_as_configured(self, identifier: str, quote_key: str) -> str:
@@ -827,7 +847,10 @@ class BaseAdapter(metaclass=AdapterMeta):
elif quote_config is None: elif quote_config is None:
pass pass
else: else:
raise QuoteConfigTypeError(quote_config) raise_compiler_error(
f'The seed configuration value of "quote_columns" has an '
f"invalid type {type(quote_config)}"
)
if quote_columns: if quote_columns:
return self.quote(column) return self.quote(column)
@@ -848,7 +871,7 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_text_type` is not implemented for this adapter!") raise NotImplementedException("`convert_text_type` is not implemented for this adapter!")
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
@@ -860,7 +883,7 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_number_type` is not implemented for this adapter!") raise NotImplementedException("`convert_number_type` is not implemented for this adapter!")
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
@@ -872,7 +895,9 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_boolean_type` is not implemented for this adapter!") raise NotImplementedException(
"`convert_boolean_type` is not implemented for this adapter!"
)
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
@@ -884,7 +909,9 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_datetime_type` is not implemented for this adapter!") raise NotImplementedException(
"`convert_datetime_type` is not implemented for this adapter!"
)
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
@@ -896,7 +923,7 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_date_type` is not implemented for this adapter!") raise NotImplementedException("`convert_date_type` is not implemented for this adapter!")
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
@@ -908,7 +935,7 @@ class BaseAdapter(metaclass=AdapterMeta):
:param col_idx: The index into the agate table for the column. :param col_idx: The index into the agate table for the column.
:return: The name of the type in the database :return: The name of the type in the database
""" """
raise NotImplementedError("`convert_time_type` is not implemented for this adapter!") raise NotImplementedException("`convert_time_type` is not implemented for this adapter!")
@available @available
@classmethod @classmethod
@@ -975,7 +1002,7 @@ class BaseAdapter(metaclass=AdapterMeta):
else: else:
package_name = 'the "{}" package'.format(project) package_name = 'the "{}" package'.format(project)
raise DbtRuntimeError( raise RuntimeException(
'dbt could not find a macro with the name "{}" in {}'.format( 'dbt could not find a macro with the name "{}" in {}'.format(
macro_name, package_name macro_name, package_name
) )
@@ -1073,7 +1100,11 @@ class BaseAdapter(metaclass=AdapterMeta):
# now we have a 1-row table of the maximum `loaded_at_field` value and # now we have a 1-row table of the maximum `loaded_at_field` value and
# the current time according to the db. # the current time according to the db.
if len(table) != 1 or len(table[0]) != 2: if len(table) != 1 or len(table[0]) != 2:
raise MacroResultError(FRESHNESS_MACRO_NAME, table) raise_compiler_error(
'Got an invalid result from "{}" macro: {}'.format(
FRESHNESS_MACRO_NAME, [tuple(r) for r in table]
)
)
if table[0][0] is None: if table[0][0] is None:
# no records in the table, so really the max_loaded_at was # no records in the table, so really the max_loaded_at was
# infinitely long ago. Just call it 0:00 January 1 year UTC # infinitely long ago. Just call it 0:00 January 1 year UTC
@@ -1150,7 +1181,7 @@ class BaseAdapter(metaclass=AdapterMeta):
elif location == "prepend": elif location == "prepend":
return f"'{value}' || {add_to}" return f"'{value}' || {add_to}"
else: else:
raise DbtRuntimeError(f'Got an unexpected location value of "{location}"') raise RuntimeException(f'Got an unexpected location value of "{location}"')
def get_rows_different_sql( def get_rows_different_sql(
self, self,
@@ -1208,7 +1239,7 @@ class BaseAdapter(metaclass=AdapterMeta):
return self.generate_python_submission_response(submission_result) return self.generate_python_submission_response(submission_result)
def generate_python_submission_response(self, submission_result: Any) -> AdapterResponse: def generate_python_submission_response(self, submission_result: Any) -> AdapterResponse:
raise NotImplementedError( raise NotImplementedException(
"Your adapter need to implement generate_python_submission_response" "Your adapter need to implement generate_python_submission_response"
) )
@@ -1232,7 +1263,7 @@ class BaseAdapter(metaclass=AdapterMeta):
valid_strategies.append("default") valid_strategies.append("default")
builtin_strategies = self.builtin_incremental_strategies() builtin_strategies = self.builtin_incremental_strategies()
if strategy in builtin_strategies and strategy not in valid_strategies: if strategy in builtin_strategies and strategy not in valid_strategies:
raise DbtRuntimeError( raise RuntimeException(
f"The incremental strategy '{strategy}' is not valid for this adapter" f"The incremental strategy '{strategy}' is not valid for this adapter"
) )
@@ -1240,7 +1271,7 @@ class BaseAdapter(metaclass=AdapterMeta):
macro_name = f"get_incremental_{strategy}_sql" macro_name = f"get_incremental_{strategy}_sql"
# The model_context should have MacroGenerator callable objects for all macros # The model_context should have MacroGenerator callable objects for all macros
if macro_name not in model_context: if macro_name not in model_context:
raise DbtRuntimeError( raise RuntimeException(
'dbt could not find an incremental strategy macro with the name "{}" in {}'.format( 'dbt could not find an incremental strategy macro with the name "{}" in {}'.format(
macro_name, self.config.project_name macro_name, self.config.project_name
) )
@@ -1296,7 +1327,7 @@ def catch_as_completed(
elif isinstance(exc, KeyboardInterrupt) or not isinstance(exc, Exception): elif isinstance(exc, KeyboardInterrupt) or not isinstance(exc, Exception):
raise exc raise exc
else: else:
warn_or_error(CatalogGenerationError(exc=str(exc))) warn_or_error(f"Encountered an error while generating catalog: {str(exc)}")
# exc is not None, derives from Exception, and isn't ctrl+c # exc is not None, derives from Exception, and isn't ctrl+c
exceptions.append(exc) exceptions.append(exc)
return merge_tables(tables), exceptions return merge_tables(tables), exceptions

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,28 @@
import re
import threading import threading
from copy import deepcopy from copy import deepcopy
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
from dbt.adapters.reference_keys import ( from dbt.adapters.reference_keys import _make_key, _ReferenceKey
_make_ref_key, import dbt.exceptions
_make_ref_key_msg, from dbt.events.functions import fire_event
_make_msg_from_ref_key, from dbt.events.types import (
_ReferenceKey, AddLink,
AddRelation,
DropCascade,
DropMissingRelation,
DropRelation,
DumpAfterAddGraph,
DumpAfterRenameSchema,
DumpBeforeAddGraph,
DumpBeforeRenameSchema,
RenameSchema,
TemporaryRelation,
UncachedRelation,
UpdateReference,
) )
from dbt.exceptions import (
DependentLinkNotCachedError,
NewNameAlreadyInCacheError,
NoneRelationFoundError,
ReferencedLinkNotCachedError,
TruncatedModelNameCausedCollisionError,
)
from dbt.events.functions import fire_event, fire_event_if
from dbt.events.types import CacheAction, CacheDumpGraph
import dbt.flags as flags
from dbt.utils import lowercase from dbt.utils import lowercase
from dbt.helper_types import Lazy
def dot_separated(key: _ReferenceKey) -> str: def dot_separated(key: _ReferenceKey) -> str:
@@ -78,7 +82,7 @@ class _CachedRelation:
:return _ReferenceKey: A key for this relation. :return _ReferenceKey: A key for this relation.
""" """
return _make_ref_key(self) return _make_key(self)
def add_reference(self, referrer: "_CachedRelation"): def add_reference(self, referrer: "_CachedRelation"):
"""Add a reference from referrer to self, indicating that if this node """Add a reference from referrer to self, indicating that if this node
@@ -141,7 +145,11 @@ class _CachedRelation:
:raises InternalError: If the new key already exists. :raises InternalError: If the new key already exists.
""" """
if new_key in self.referenced_by: if new_key in self.referenced_by:
raise NewNameAlreadyInCacheError(old_key, new_key) dbt.exceptions.raise_cache_inconsistent(
'in rename of "{}" -> "{}", new name is in the cache already'.format(
old_key, new_key
)
)
if old_key not in self.referenced_by: if old_key not in self.referenced_by:
return return
@@ -257,17 +265,21 @@ class RelationsCache:
if referenced is None: if referenced is None:
return return
if referenced is None: if referenced is None:
raise ReferencedLinkNotCachedError(referenced_key) dbt.exceptions.raise_cache_inconsistent(
"in add_link, referenced link key {} not in cache!".format(referenced_key)
)
dependent = self.relations.get(dependent_key) dependent = self.relations.get(dependent_key)
if dependent is None: if dependent is None:
raise DependentLinkNotCachedError(dependent_key) dbt.exceptions.raise_cache_inconsistent(
"in add_link, dependent link key {} not in cache!".format(dependent_key)
)
assert dependent is not None # we just raised! assert dependent is not None # we just raised!
referenced.add_reference(dependent) referenced.add_reference(dependent)
# This is called in plugins/postgres/dbt/adapters/postgres/impl.py # TODO: Is this dead code? I can't seem to find it grepping the codebase.
def add_link(self, referenced, dependent): def add_link(self, referenced, dependent):
"""Add a link between two relations to the database. If either relation """Add a link between two relations to the database. If either relation
does not exist, it will be added as an "external" relation. does not exist, it will be added as an "external" relation.
@@ -282,18 +294,13 @@ class RelationsCache:
:param BaseRelation dependent: The dependent model. :param BaseRelation dependent: The dependent model.
:raises InternalError: If either entry does not exist. :raises InternalError: If either entry does not exist.
""" """
ref_key = _make_ref_key(referenced) ref_key = _make_key(referenced)
dep_key = _make_ref_key(dependent) dep_key = _make_key(dependent)
if (ref_key.database, ref_key.schema) not in self: if (ref_key.database, ref_key.schema) not in self:
# if we have not cached the referenced schema at all, we must be # if we have not cached the referenced schema at all, we must be
# referring to a table outside our control. There's no need to make # referring to a table outside our control. There's no need to make
# a link - we will never drop the referenced relation during a run. # a link - we will never drop the referenced relation during a run.
fire_event( fire_event(UncachedRelation(dep_key=dep_key, ref_key=ref_key))
CacheAction(
ref_key=_make_msg_from_ref_key(ref_key),
ref_key_2=_make_msg_from_ref_key(dep_key),
)
)
return return
if ref_key not in self.relations: if ref_key not in self.relations:
# Insert a dummy "external" relation. # Insert a dummy "external" relation.
@@ -303,13 +310,7 @@ class RelationsCache:
# Insert a dummy "external" relation. # Insert a dummy "external" relation.
dependent = dependent.replace(type=referenced.External) dependent = dependent.replace(type=referenced.External)
self.add(dependent) self.add(dependent)
fire_event( fire_event(AddLink(dep_key=dep_key, ref_key=ref_key))
CacheAction(
action="add_link",
ref_key=_make_msg_from_ref_key(dep_key),
ref_key_2=_make_msg_from_ref_key(ref_key),
)
)
with self.lock: with self.lock:
self._add_link(ref_key, dep_key) self._add_link(ref_key, dep_key)
@@ -320,18 +321,12 @@ class RelationsCache:
:param BaseRelation relation: The underlying relation. :param BaseRelation relation: The underlying relation.
""" """
cached = _CachedRelation(relation) cached = _CachedRelation(relation)
fire_event_if( fire_event(AddRelation(relation=_make_key(cached)))
flags.LOG_CACHE_EVENTS, fire_event(DumpBeforeAddGraph(dump=Lazy.defer(lambda: self.dump_graph())))
lambda: CacheDumpGraph(before_after="before", action="adding", dump=self.dump_graph()),
)
fire_event(CacheAction(action="add_relation", ref_key=_make_ref_key_msg(cached)))
with self.lock: with self.lock:
self._setdefault(cached) self._setdefault(cached)
fire_event_if( fire_event(DumpAfterAddGraph(dump=Lazy.defer(lambda: self.dump_graph())))
flags.LOG_CACHE_EVENTS,
lambda: CacheDumpGraph(before_after="after", action="adding", dump=self.dump_graph()),
)
def _remove_refs(self, keys): def _remove_refs(self, keys):
"""Removes all references to all entries in keys. This does not """Removes all references to all entries in keys. This does not
@@ -346,6 +341,19 @@ class RelationsCache:
for cached in self.relations.values(): for cached in self.relations.values():
cached.release_references(keys) cached.release_references(keys)
def _drop_cascade_relation(self, dropped_key):
"""Drop the given relation and cascade it appropriately to all
dependent relations.
:param _CachedRelation dropped: An existing _CachedRelation to drop.
"""
if dropped_key not in self.relations:
fire_event(DropMissingRelation(relation=dropped_key))
return
consequences = self.relations[dropped_key].collect_consequences()
fire_event(DropCascade(dropped=dropped_key, consequences=consequences))
self._remove_refs(consequences)
def drop(self, relation): def drop(self, relation):
"""Drop the named relation and cascade it appropriately to all """Drop the named relation and cascade it appropriately to all
dependent relations. dependent relations.
@@ -357,22 +365,10 @@ class RelationsCache:
:param str schema: The schema of the relation to drop. :param str schema: The schema of the relation to drop.
:param str identifier: The identifier of the relation to drop. :param str identifier: The identifier of the relation to drop.
""" """
dropped_key = _make_ref_key(relation) dropped_key = _make_key(relation)
dropped_key_msg = _make_ref_key_msg(relation) fire_event(DropRelation(dropped=dropped_key))
fire_event(CacheAction(action="drop_relation", ref_key=dropped_key_msg))
with self.lock: with self.lock:
if dropped_key not in self.relations: self._drop_cascade_relation(dropped_key)
fire_event(CacheAction(action="drop_missing_relation", ref_key=dropped_key_msg))
return
consequences = self.relations[dropped_key].collect_consequences()
# convert from a list of _ReferenceKeys to a list of ReferenceKeyMsgs
consequence_msgs = [_make_msg_from_ref_key(key) for key in consequences]
fire_event(
CacheAction(
action="drop_cascade", ref_key=dropped_key_msg, ref_list=consequence_msgs
)
)
self._remove_refs(consequences)
def _rename_relation(self, old_key, new_relation): def _rename_relation(self, old_key, new_relation):
"""Rename a relation named old_key to new_key, updating references. """Rename a relation named old_key to new_key, updating references.
@@ -394,14 +390,8 @@ class RelationsCache:
for cached in self.relations.values(): for cached in self.relations.values():
if cached.is_referenced_by(old_key): if cached.is_referenced_by(old_key):
fire_event( fire_event(
CacheAction( UpdateReference(old_key=old_key, new_key=new_key, cached_key=cached.key())
action="update_reference",
ref_key=_make_ref_key_msg(old_key),
ref_key_2=_make_ref_key_msg(new_key),
ref_key_3=_make_ref_key_msg(cached.key()),
)
) )
cached.rename_key(old_key, new_key) cached.rename_key(old_key, new_key)
self.relations[new_key] = relation self.relations[new_key] = relation
@@ -426,12 +416,27 @@ class RelationsCache:
if new_key in self.relations: if new_key in self.relations:
# Tell user when collision caused by model names truncated during # Tell user when collision caused by model names truncated during
# materialization. # materialization.
raise TruncatedModelNameCausedCollisionError(new_key, self.relations) match = re.search("__dbt_backup|__dbt_tmp$", new_key.identifier)
if match:
truncated_model_name_prefix = new_key.identifier[: match.start()]
message_addendum = (
"\n\nName collisions can occur when the length of two "
"models' names approach your database's builtin limit. "
"Try restructuring your project such that no two models "
"share the prefix '{}'.".format(truncated_model_name_prefix)
+ " Then, clean your warehouse of any removed models."
)
else:
message_addendum = ""
dbt.exceptions.raise_cache_inconsistent(
"in rename, new key {} already in cache: {}{}".format(
new_key, list(self.relations.keys()), message_addendum
)
)
if old_key not in self.relations: if old_key not in self.relations:
fire_event( fire_event(TemporaryRelation(key=old_key))
CacheAction(action="temporary_relation", ref_key=_make_msg_from_ref_key(old_key))
)
return False return False
return True return True
@@ -447,20 +452,11 @@ class RelationsCache:
:param BaseRelation new: The new relation name information. :param BaseRelation new: The new relation name information.
:raises InternalError: If the new key is already present. :raises InternalError: If the new key is already present.
""" """
old_key = _make_ref_key(old) old_key = _make_key(old)
new_key = _make_ref_key(new) new_key = _make_key(new)
fire_event( fire_event(RenameSchema(old_key=old_key, new_key=new_key))
CacheAction(
action="rename_relation",
ref_key=_make_msg_from_ref_key(old_key),
ref_key_2=_make_msg_from_ref_key(new),
)
)
fire_event_if( fire_event(DumpBeforeRenameSchema(dump=Lazy.defer(lambda: self.dump_graph())))
flags.LOG_CACHE_EVENTS,
lambda: CacheDumpGraph(before_after="before", action="rename", dump=self.dump_graph()),
)
with self.lock: with self.lock:
if self._check_rename_constraints(old_key, new_key): if self._check_rename_constraints(old_key, new_key):
@@ -468,10 +464,7 @@ class RelationsCache:
else: else:
self._setdefault(_CachedRelation(new)) self._setdefault(_CachedRelation(new))
fire_event_if( fire_event(DumpAfterRenameSchema(dump=Lazy.defer(lambda: self.dump_graph())))
flags.LOG_CACHE_EVENTS,
lambda: CacheDumpGraph(before_after="after", action="rename", dump=self.dump_graph()),
)
def get_relations(self, database: Optional[str], schema: Optional[str]) -> List[Any]: def get_relations(self, database: Optional[str], schema: Optional[str]) -> List[Any]:
"""Case-insensitively yield all relations matching the given schema. """Case-insensitively yield all relations matching the given schema.
@@ -490,7 +483,9 @@ class RelationsCache:
] ]
if None in results: if None in results:
raise NoneRelationFoundError() dbt.exceptions.raise_cache_inconsistent(
"in get_relations, a None relation was found in the cache!"
)
return results return results
def clear(self): def clear(self):
@@ -517,6 +512,6 @@ class RelationsCache:
""" """
for relation in to_remove: for relation in to_remove:
# it may have been cascaded out already # it may have been cascaded out already
drop_key = _make_ref_key(relation) drop_key = _make_key(relation)
if drop_key in self.relations: if drop_key in self.relations:
self.drop(drop_key) self.drop(drop_key)

View File

@@ -1,5 +1,4 @@
import threading import threading
import traceback
from contextlib import contextmanager from contextlib import contextmanager
from importlib import import_module from importlib import import_module
from pathlib import Path from pathlib import Path
@@ -10,7 +9,7 @@ from dbt.adapters.protocol import AdapterConfig, AdapterProtocol, RelationProtoc
from dbt.contracts.connection import AdapterRequiredConfig, Credentials from dbt.contracts.connection import AdapterRequiredConfig, Credentials
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import AdapterImportError, PluginLoadError from dbt.events.types import AdapterImportError, PluginLoadError
from dbt.exceptions import DbtInternalError, DbtRuntimeError from dbt.exceptions import InternalException, RuntimeException
from dbt.include.global_project import PACKAGE_PATH as GLOBAL_PROJECT_PATH from dbt.include.global_project import PACKAGE_PATH as GLOBAL_PROJECT_PATH
from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
@@ -34,7 +33,7 @@ class AdapterContainer:
names = ", ".join(self.plugins.keys()) names = ", ".join(self.plugins.keys())
message = f"Invalid adapter type {name}! Must be one of {names}" message = f"Invalid adapter type {name}! Must be one of {names}"
raise DbtRuntimeError(message) raise RuntimeException(message)
def get_adapter_class_by_name(self, name: str) -> Type[Adapter]: def get_adapter_class_by_name(self, name: str) -> Type[Adapter]:
plugin = self.get_plugin_by_name(name) plugin = self.get_plugin_by_name(name)
@@ -60,17 +59,17 @@ class AdapterContainer:
# the user about it via a runtime error # the user about it via a runtime error
if exc.name == "dbt.adapters." + name: if exc.name == "dbt.adapters." + name:
fire_event(AdapterImportError(exc=str(exc))) fire_event(AdapterImportError(exc=str(exc)))
raise DbtRuntimeError(f"Could not find adapter type {name}!") raise RuntimeException(f"Could not find adapter type {name}!")
# otherwise, the error had to have come from some underlying # otherwise, the error had to have come from some underlying
# library. Log the stack trace. # library. Log the stack trace.
fire_event(PluginLoadError(exc_info=traceback.format_exc())) fire_event(PluginLoadError())
raise raise
plugin: AdapterPlugin = mod.Plugin plugin: AdapterPlugin = mod.Plugin
plugin_type = plugin.adapter.type() plugin_type = plugin.adapter.type()
if plugin_type != name: if plugin_type != name:
raise DbtRuntimeError( raise RuntimeException(
f"Expected to find adapter with type named {name}, got " f"Expected to find adapter with type named {name}, got "
f"adapter with type {plugin_type}" f"adapter with type {plugin_type}"
) )
@@ -132,7 +131,7 @@ class AdapterContainer:
try: try:
plugin = self.plugins[plugin_name] plugin = self.plugins[plugin_name]
except KeyError: except KeyError:
raise DbtInternalError(f"No plugin found for {plugin_name}") from None raise InternalException(f"No plugin found for {plugin_name}") from None
plugins.append(plugin) plugins.append(plugin)
seen.add(plugin_name) seen.add(plugin_name)
for dep in plugin.dependencies: for dep in plugin.dependencies:
@@ -151,7 +150,7 @@ class AdapterContainer:
try: try:
path = self.packages[package_name] path = self.packages[package_name]
except KeyError: except KeyError:
raise DbtInternalError(f"No internal package listing found for {package_name}") raise InternalException(f"No internal package listing found for {package_name}")
paths.append(path) paths.append(path)
return paths return paths

View File

@@ -8,6 +8,7 @@ from typing import (
Generic, Generic,
TypeVar, TypeVar,
Tuple, Tuple,
Union,
Dict, Dict,
Any, Any,
) )
@@ -16,7 +17,8 @@ from typing_extensions import Protocol
import agate import agate
from dbt.contracts.connection import Connection, AdapterRequiredConfig, AdapterResponse from dbt.contracts.connection import Connection, AdapterRequiredConfig, AdapterResponse
from dbt.contracts.graph.nodes import ResultNode, ManifestNode from dbt.contracts.graph.compiled import CompiledNode, ManifestNode, NonSourceCompiledNode
from dbt.contracts.graph.parsed import ParsedNode, ParsedSourceDefinition
from dbt.contracts.graph.model_config import BaseConfig from dbt.contracts.graph.model_config import BaseConfig
from dbt.contracts.graph.manifest import Manifest from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.relation import Policy, HasQuoting from dbt.contracts.relation import Policy, HasQuoting
@@ -46,7 +48,11 @@ class RelationProtocol(Protocol):
... ...
@classmethod @classmethod
def create_from(cls: Type[Self], config: HasQuoting, node: ResultNode) -> Self: def create_from(
cls: Type[Self],
config: HasQuoting,
node: Union[CompiledNode, ParsedNode, ParsedSourceDefinition],
) -> Self:
... ...
@@ -59,7 +65,7 @@ class CompilerProtocol(Protocol):
node: ManifestNode, node: ManifestNode,
manifest: Manifest, manifest: Manifest,
extra_context: Optional[Dict[str, Any]] = None, extra_context: Optional[Dict[str, Any]] = None,
) -> ManifestNode: ) -> NonSourceCompiledNode:
... ...

View File

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

View File

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

View File

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

View File

@@ -31,6 +31,7 @@ def cli_runner():
@p.cache_selected_only @p.cache_selected_only
@p.debug @p.debug
@p.enable_legacy_logger @p.enable_legacy_logger
@p.event_buffer_size
@p.fail_fast @p.fail_fast
@p.log_cache_events @p.log_cache_events
@p.log_format @p.log_format
@@ -46,7 +47,6 @@ def cli_runner():
@p.version @p.version
@p.version_check @p.version_check
@p.warn_error @p.warn_error
@p.warn_error_options
@p.write_json @p.write_json
def cli(ctx, **kwargs): def cli(ctx, **kwargs):
"""An ELT tool for managing your SQL transformations and data models. """An ELT tool for managing your SQL transformations and data models.

View File

@@ -1,8 +1,6 @@
from click import ParamType from click import ParamType
import yaml import yaml
from dbt.helper_types import WarnErrorOptions
class YAML(ParamType): class YAML(ParamType):
"""The Click YAML type. Converts YAML strings into objects.""" """The Click YAML type. Converts YAML strings into objects."""
@@ -19,19 +17,6 @@ class YAML(ParamType):
self.fail(f"String '{value}' is not valid YAML", param, ctx) self.fail(f"String '{value}' is not valid YAML", param, ctx)
class WarnErrorOptionsType(YAML):
"""The Click WarnErrorOptions type. Converts YAML strings into objects."""
name = "WarnErrorOptionsType"
def convert(self, value, param, ctx):
include_exclude = super().convert(value, param, ctx)
return WarnErrorOptions(
include=include_exclude.get("include", []), exclude=include_exclude.get("exclude", [])
)
class Truthy(ParamType): class Truthy(ParamType):
"""The Click Truthy type. Converts strings into a "truthy" type""" """The Click Truthy type. Converts strings into a "truthy" type"""

View File

@@ -1,7 +1,7 @@
from pathlib import Path, PurePath from pathlib import Path, PurePath
import click import click
from dbt.cli.option_types import YAML, WarnErrorOptionsType from dbt.cli.option_types import YAML
from dbt.cli.resolvers import default_project_dir, default_profiles_dir from dbt.cli.resolvers import default_project_dir, default_profiles_dir
@@ -80,6 +80,14 @@ enable_legacy_logger = click.option(
hidden=True, hidden=True,
) )
event_buffer_size = click.option(
"--event-buffer-size",
envvar="DBT_EVENT_BUFFER_SIZE",
help="Sets the max number of events to buffer in EVENT_HISTORY.",
default=100000,
type=click.INT,
)
exclude = click.option("--exclude", envvar=None, help="Specify the nodes to exclude.") exclude = click.option("--exclude", envvar=None, help="Specify the nodes to exclude.")
fail_fast = click.option( fail_fast = click.option(
@@ -270,7 +278,7 @@ show = click.option(
) )
skip_profile_setup = click.option( skip_profile_setup = click.option(
"--skip-profile-setup", "-s", envvar=None, help="Skip interactive profile setup.", is_flag=True "--skip-profile-setup", "-s", envvar=None, help="Skip interative profile setup.", is_flag=True
) )
# TODO: The env var and name (reflected in flags) are corrections! # TODO: The env var and name (reflected in flags) are corrections!
@@ -358,20 +366,9 @@ version_check = click.option(
) )
warn_error = click.option( warn_error = click.option(
"--warn-error", "--warn-error/--no-warn-error",
envvar="DBT_WARN_ERROR", envvar="DBT_WARN_ERROR",
help="If dbt would normally warn, instead raise an exception. Examples include --select that selects nothing, deprecations, configurations with no associated models, invalid test configurations, and missing sources/refs in tests.", help="If dbt would normally warn, instead raise an exception. Examples include --models that selects nothing, deprecations, configurations with no associated models, invalid test configurations, and missing sources/refs in tests.",
default=None,
flag_value=True,
)
warn_error_options = click.option(
"--warn-error-options",
envvar="DBT_WARN_ERROR_OPTIONS",
default=None,
help="""If dbt would normally warn, instead raise an exception based on include/exclude configuration. Examples include --select that selects nothing, deprecations, configurations with no associated models, invalid test configurations,
and missing sources/refs in tests. This argument should be a YAML string, with keys 'include' or 'exclude'. eg. '{"include": "all", "exclude": ["NoNodesForSelectionCriteria"]}'""",
type=WarnErrorOptionsType(),
) )
write_json = click.option( write_json = click.option(

View File

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

View File

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

View File

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

View File

@@ -25,20 +25,17 @@ from dbt.utils import (
) )
from dbt.clients._jinja_blocks import BlockIterator, BlockData, BlockTag from dbt.clients._jinja_blocks import BlockIterator, BlockData, BlockTag
from dbt.contracts.graph.nodes import GenericTestNode from dbt.contracts.graph.compiled import CompiledGenericTestNode
from dbt.contracts.graph.parsed import ParsedGenericTestNode
from dbt.exceptions import ( from dbt.exceptions import (
CaughtMacroError, InternalException,
CaughtMacroErrorWithNodeError, raise_compiler_error,
CompilationError, CompilationException,
DbtInternalError, invalid_materialization_argument,
MaterializationArgError,
JinjaRenderingError,
MacroReturn, MacroReturn,
MaterializtionMacroNotUsedError, JinjaRenderingException,
NoSupportedLanguagesFoundError, UndefinedMacroException,
UndefinedCompilationError,
UndefinedMacroError,
) )
from dbt import flags from dbt import flags
from dbt.node_types import ModelLanguage from dbt.node_types import ModelLanguage
@@ -161,9 +158,9 @@ def quoted_native_concat(nodes):
except (ValueError, SyntaxError, MemoryError): except (ValueError, SyntaxError, MemoryError):
result = raw result = raw
if isinstance(raw, BoolMarker) and not isinstance(result, bool): if isinstance(raw, BoolMarker) and not isinstance(result, bool):
raise JinjaRenderingError(f"Could not convert value '{raw!s}' into type 'bool'") raise JinjaRenderingException(f"Could not convert value '{raw!s}' into type 'bool'")
if isinstance(raw, NumberMarker) and not _is_number(result): if isinstance(raw, NumberMarker) and not _is_number(result):
raise JinjaRenderingError(f"Could not convert value '{raw!s}' into type 'number'") raise JinjaRenderingException(f"Could not convert value '{raw!s}' into type 'number'")
return result return result
@@ -241,12 +238,12 @@ class BaseMacroGenerator:
try: try:
yield yield
except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e: except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e:
raise CaughtMacroError(e) raise_compiler_error(str(e))
def call_macro(self, *args, **kwargs): def call_macro(self, *args, **kwargs):
# called from __call__ methods # called from __call__ methods
if self.context is None: if self.context is None:
raise DbtInternalError("Context is still None in call_macro!") raise InternalException("Context is still None in call_macro!")
assert self.context is not None assert self.context is not None
macro = self.get_macro() macro = self.get_macro()
@@ -273,7 +270,7 @@ class MacroStack(threading.local):
def pop(self, name): def pop(self, name):
got = self.call_stack.pop() got = self.call_stack.pop()
if got != name: if got != name:
raise DbtInternalError(f"popped {got}, expected {name}") raise InternalException(f"popped {got}, expected {name}")
class MacroGenerator(BaseMacroGenerator): class MacroGenerator(BaseMacroGenerator):
@@ -300,8 +297,8 @@ class MacroGenerator(BaseMacroGenerator):
try: try:
yield yield
except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e: except (TypeError, jinja2.exceptions.TemplateRuntimeError) as e:
raise CaughtMacroErrorWithNodeError(exc=e, node=self.macro) raise_compiler_error(str(e), self.macro)
except CompilationError as e: except CompilationException as e:
e.stack.append(self.macro) e.stack.append(self.macro)
raise e raise e
@@ -380,7 +377,7 @@ class MaterializationExtension(jinja2.ext.Extension):
node.defaults.append(languages) node.defaults.append(languages)
else: else:
raise MaterializationArgError(materialization_name, target.name) invalid_materialization_argument(materialization_name, target.name)
if SUPPORTED_LANG_ARG not in node.args: if SUPPORTED_LANG_ARG not in node.args:
node.args.append(SUPPORTED_LANG_ARG) node.args.append(SUPPORTED_LANG_ARG)
@@ -455,7 +452,7 @@ def create_undefined(node=None):
return self return self
def __reduce__(self): def __reduce__(self):
raise UndefinedCompilationError(name=self.name, node=node) raise_compiler_error(f"{self.name} is undefined", node=node)
return Undefined return Undefined
@@ -513,10 +510,10 @@ def catch_jinja(node=None) -> Iterator[None]:
yield yield
except jinja2.exceptions.TemplateSyntaxError as e: except jinja2.exceptions.TemplateSyntaxError as e:
e.translated = False e.translated = False
raise CompilationError(str(e), node) from e raise CompilationException(str(e), node) from e
except jinja2.exceptions.UndefinedError as e: except jinja2.exceptions.UndefinedError as e:
raise UndefinedMacroError(str(e), node) from e raise UndefinedMacroException(str(e), node) from e
except CompilationError as exc: except CompilationException as exc:
exc.add_node(node) exc.add_node(node)
raise raise
@@ -623,7 +620,7 @@ GENERIC_TEST_KWARGS_NAME = "_dbt_generic_test_kwargs"
def add_rendered_test_kwargs( def add_rendered_test_kwargs(
context: Dict[str, Any], context: Dict[str, Any],
node: GenericTestNode, node: Union[ParsedGenericTestNode, CompiledGenericTestNode],
capture_macros: bool = False, capture_macros: bool = False,
) -> None: ) -> None:
"""Render each of the test kwargs in the given context using the native """Render each of the test kwargs in the given context using the native
@@ -655,13 +652,13 @@ def add_rendered_test_kwargs(
def get_supported_languages(node: jinja2.nodes.Macro) -> List[ModelLanguage]: def get_supported_languages(node: jinja2.nodes.Macro) -> List[ModelLanguage]:
if "materialization" not in node.name: if "materialization" not in node.name:
raise MaterializtionMacroNotUsedError(node=node) raise_compiler_error("Only materialization macros can be used with this function")
no_kwargs = not node.defaults no_kwargs = not node.defaults
no_langs_found = SUPPORTED_LANG_ARG not in node.args no_langs_found = SUPPORTED_LANG_ARG not in node.args
if no_kwargs or no_langs_found: if no_kwargs or no_langs_found:
raise NoSupportedLanguagesFoundError(node=node) raise_compiler_error(f"No supported_languages found in materialization macro {node.name}")
lang_idx = node.args.index(SUPPORTED_LANG_ARG) lang_idx = node.args.index(SUPPORTED_LANG_ARG)
# indexing defaults from the end # indexing defaults from the end

View File

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

View File

@@ -3,9 +3,9 @@ from typing import Any, Dict, List
import requests import requests
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import ( from dbt.events.types import (
RegistryProgressGETRequest, RegistryProgressMakingGETRequest,
RegistryProgressGETResponse, RegistryProgressGETResponse,
RegistryIndexProgressGETRequest, RegistryIndexProgressMakingGETRequest,
RegistryIndexProgressGETResponse, RegistryIndexProgressGETResponse,
RegistryResponseUnexpectedType, RegistryResponseUnexpectedType,
RegistryResponseMissingTopKeys, RegistryResponseMissingTopKeys,
@@ -38,7 +38,7 @@ def _get_with_retries(package_name, registry_base_url=None):
def _get(package_name, registry_base_url=None): def _get(package_name, registry_base_url=None):
url = _get_url(package_name, registry_base_url) url = _get_url(package_name, registry_base_url)
fire_event(RegistryProgressGETRequest(url=url)) fire_event(RegistryProgressMakingGETRequest(url=url))
# all exceptions from requests get caught in the retry logic so no need to wrap this here # all exceptions from requests get caught in the retry logic so no need to wrap this here
resp = requests.get(url, timeout=30) resp = requests.get(url, timeout=30)
fire_event(RegistryProgressGETResponse(url=url, resp_code=resp.status_code)) fire_event(RegistryProgressGETResponse(url=url, resp_code=resp.status_code))
@@ -162,7 +162,7 @@ def get_compatible_versions(package_name, dbt_version, should_version_check) ->
def _get_index(registry_base_url=None): def _get_index(registry_base_url=None):
url = _get_url("index", registry_base_url) url = _get_url("index", registry_base_url)
fire_event(RegistryIndexProgressGETRequest(url=url)) fire_event(RegistryIndexProgressMakingGETRequest(url=url))
# all exceptions from requests get caught in the retry logic so no need to wrap this here # all exceptions from requests get caught in the retry logic so no need to wrap this here
resp = requests.get(url, timeout=30) resp = requests.get(url, timeout=30)
fire_event(RegistryIndexProgressGETResponse(url=url, resp_code=resp.status_code)) fire_event(RegistryIndexProgressGETResponse(url=url, resp_code=resp.status_code))

View File

@@ -19,8 +19,8 @@ from dbt.events.types import (
SystemErrorRetrievingModTime, SystemErrorRetrievingModTime,
SystemCouldNotWrite, SystemCouldNotWrite,
SystemExecutingCmd, SystemExecutingCmd,
SystemStdOut, SystemStdOutMsg,
SystemStdErr, SystemStdErrMsg,
SystemReportReturnCode, SystemReportReturnCode,
) )
import dbt.exceptions import dbt.exceptions
@@ -144,8 +144,7 @@ def make_symlink(source: str, link_path: str) -> None:
Create a symlink at `link_path` referring to `source`. Create a symlink at `link_path` referring to `source`.
""" """
if not supports_symlinks(): if not supports_symlinks():
# TODO: why not import these at top? dbt.exceptions.system_error("create a symbolic link")
raise dbt.exceptions.SymbolicLinkError()
os.symlink(source, link_path) os.symlink(source, link_path)
@@ -412,7 +411,7 @@ def _interpret_oserror(exc: OSError, cwd: str, cmd: List[str]) -> NoReturn:
_handle_posix_error(exc, cwd, cmd) _handle_posix_error(exc, cwd, cmd)
# this should not be reachable, raise _something_ at least! # this should not be reachable, raise _something_ at least!
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
"Unhandled exception in _interpret_oserror: {}".format(exc) "Unhandled exception in _interpret_oserror: {}".format(exc)
) )
@@ -441,8 +440,8 @@ def run_cmd(cwd: str, cmd: List[str], env: Optional[Dict[str, Any]] = None) -> T
except OSError as exc: except OSError as exc:
_interpret_oserror(exc, cwd, cmd) _interpret_oserror(exc, cwd, cmd)
fire_event(SystemStdOut(bmsg=out)) fire_event(SystemStdOutMsg(bmsg=out))
fire_event(SystemStdErr(bmsg=err)) fire_event(SystemStdErrMsg(bmsg=err))
if proc.returncode != 0: if proc.returncode != 0:
fire_event(SystemReportReturnCode(returncode=proc.returncode)) fire_event(SystemReportReturnCode(returncode=proc.returncode))

View File

@@ -60,4 +60,4 @@ def load_yaml_text(contents, path=None):
else: else:
error = str(e) error = str(e)
raise dbt.exceptions.DbtValidationError(error) raise dbt.exceptions.ValidationException(error)

View File

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

View File

@@ -9,14 +9,12 @@ from dbt.clients.system import load_file_contents
from dbt.clients.yaml_helper import load_yaml_text from dbt.clients.yaml_helper import load_yaml_text
from dbt.contracts.connection import Credentials, HasCredentials from dbt.contracts.connection import Credentials, HasCredentials
from dbt.contracts.project import ProfileConfig, UserConfig from dbt.contracts.project import ProfileConfig, UserConfig
from dbt.exceptions import ( from dbt.exceptions import CompilationException
CompilationError, from dbt.exceptions import DbtProfileError
DbtProfileError, from dbt.exceptions import DbtProjectError
DbtProjectError, from dbt.exceptions import ValidationException
DbtValidationError, from dbt.exceptions import RuntimeException
DbtRuntimeError, from dbt.exceptions import validator_error_message
ProfileConfigError,
)
from dbt.events.types import MissingProfileTarget from dbt.events.types import MissingProfileTarget
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.utils import coerce_dict_str from dbt.utils import coerce_dict_str
@@ -60,9 +58,9 @@ def read_profile(profiles_dir: str) -> Dict[str, Any]:
msg = f"The profiles.yml file at {path} is empty" msg = f"The profiles.yml file at {path} is empty"
raise DbtProfileError(INVALID_PROFILE_MESSAGE.format(error_string=msg)) raise DbtProfileError(INVALID_PROFILE_MESSAGE.format(error_string=msg))
return yaml_content return yaml_content
except DbtValidationError as e: except ValidationException as e:
msg = INVALID_PROFILE_MESSAGE.format(error_string=e) msg = INVALID_PROFILE_MESSAGE.format(error_string=e)
raise DbtValidationError(msg) from e raise ValidationException(msg) from e
return {} return {}
@@ -75,7 +73,7 @@ def read_user_config(directory: str) -> UserConfig:
if user_config is not None: if user_config is not None:
UserConfig.validate(user_config) UserConfig.validate(user_config)
return UserConfig.from_dict(user_config) return UserConfig.from_dict(user_config)
except (DbtRuntimeError, ValidationError): except (RuntimeException, ValidationError):
pass pass
return UserConfig() return UserConfig()
@@ -158,7 +156,7 @@ class Profile(HasCredentials):
dct = self.to_profile_info(serialize_credentials=True) dct = self.to_profile_info(serialize_credentials=True)
ProfileConfig.validate(dct) ProfileConfig.validate(dct)
except ValidationError as exc: except ValidationError as exc:
raise ProfileConfigError(exc) from exc raise DbtProfileError(validator_error_message(exc)) from exc
@staticmethod @staticmethod
def _credentials_from_profile( def _credentials_from_profile(
@@ -182,8 +180,8 @@ class Profile(HasCredentials):
data = cls.translate_aliases(profile) data = cls.translate_aliases(profile)
cls.validate(data) cls.validate(data)
credentials = cls.from_dict(data) credentials = cls.from_dict(data)
except (DbtRuntimeError, ValidationError) as e: except (RuntimeException, ValidationError) as e:
msg = str(e) if isinstance(e, DbtRuntimeError) else e.message msg = str(e) if isinstance(e, RuntimeException) else e.message
raise DbtProfileError( raise DbtProfileError(
'Credentials in profile "{}", target "{}" invalid: {}'.format( 'Credentials in profile "{}", target "{}" invalid: {}'.format(
profile_name, target_name, msg profile_name, target_name, msg
@@ -299,7 +297,7 @@ class Profile(HasCredentials):
try: try:
profile_data = renderer.render_data(raw_profile_data) profile_data = renderer.render_data(raw_profile_data)
except CompilationError as exc: except CompilationException as exc:
raise DbtProfileError(str(exc)) from exc raise DbtProfileError(str(exc)) from exc
return target_name, profile_data return target_name, profile_data

View File

@@ -16,19 +16,19 @@ import hashlib
import os import os
from dbt import flags, deprecations from dbt import flags, deprecations
from dbt.clients.system import path_exists, resolve_path_from_base, load_file_contents from dbt.clients.system import resolve_path_from_base
from dbt.clients.system import path_exists
from dbt.clients.system import load_file_contents
from dbt.clients.yaml_helper import load_yaml_text from dbt.clients.yaml_helper import load_yaml_text
from dbt.contracts.connection import QueryComment from dbt.contracts.connection import QueryComment
from dbt.exceptions import ( from dbt.exceptions import DbtProjectError
DbtProjectError, from dbt.exceptions import SemverException
SemverError, from dbt.exceptions import validator_error_message
ProjectContractBrokenError, from dbt.exceptions import RuntimeException
ProjectContractError,
DbtRuntimeError,
)
from dbt.graph import SelectionSpec from dbt.graph import SelectionSpec
from dbt.helper_types import NoValue from dbt.helper_types import NoValue
from dbt.semver import VersionSpecifier, versions_compatible from dbt.semver import VersionSpecifier
from dbt.semver import versions_compatible
from dbt.version import get_installed_version from dbt.version import get_installed_version
from dbt.utils import MultiDict from dbt.utils import MultiDict
from dbt.node_types import NodeType from dbt.node_types import NodeType
@@ -219,7 +219,7 @@ def _get_required_version(
try: try:
dbt_version = _parse_versions(dbt_raw_version) dbt_version = _parse_versions(dbt_raw_version)
except SemverError as e: except SemverException as e:
raise DbtProjectError(str(e)) from e raise DbtProjectError(str(e)) from e
if verify_version: if verify_version:
@@ -248,7 +248,7 @@ class PartialProject(RenderComponents):
project_name: Optional[str] = field( project_name: Optional[str] = field(
metadata=dict( metadata=dict(
description=( description=(
"The name of the project. This should always be set and will not be rendered" "The name of the project. This should always be set and will not " "be rendered"
) )
) )
) )
@@ -325,7 +325,7 @@ class PartialProject(RenderComponents):
ProjectContract.validate(rendered.project_dict) ProjectContract.validate(rendered.project_dict)
cfg = ProjectContract.from_dict(rendered.project_dict) cfg = ProjectContract.from_dict(rendered.project_dict)
except ValidationError as e: except ValidationError as e:
raise ProjectContractError(e) from e raise DbtProjectError(validator_error_message(e)) from e
# name/version are required in the Project definition, so we can assume # name/version are required in the Project definition, so we can assume
# they are present # they are present
name = cfg.name name = cfg.name
@@ -381,7 +381,6 @@ class PartialProject(RenderComponents):
sources: Dict[str, Any] sources: Dict[str, Any]
tests: Dict[str, Any] tests: Dict[str, Any]
metrics: Dict[str, Any] metrics: Dict[str, Any]
entities: Dict[str, Any]
exposures: Dict[str, Any] exposures: Dict[str, Any]
vars_value: VarProvider vars_value: VarProvider
@@ -392,7 +391,6 @@ class PartialProject(RenderComponents):
sources = cfg.sources sources = cfg.sources
tests = cfg.tests tests = cfg.tests
metrics = cfg.metrics metrics = cfg.metrics
entities = cfg.entities
exposures = cfg.exposures exposures = cfg.exposures
if cfg.vars is None: if cfg.vars is None:
vars_dict: Dict[str, Any] = {} vars_dict: Dict[str, Any] = {}
@@ -448,7 +446,6 @@ class PartialProject(RenderComponents):
sources=sources, sources=sources,
tests=tests, tests=tests,
metrics=metrics, metrics=metrics,
entities=entities,
exposures=exposures, exposures=exposures,
vars=vars_value, vars=vars_value,
config_version=cfg.config_version, config_version=cfg.config_version,
@@ -553,7 +550,6 @@ class Project:
sources: Dict[str, Any] sources: Dict[str, Any]
tests: Dict[str, Any] tests: Dict[str, Any]
metrics: Dict[str, Any] metrics: Dict[str, Any]
entities: Dict[str, Any]
exposures: Dict[str, Any] exposures: Dict[str, Any]
vars: VarProvider vars: VarProvider
dbt_version: List[VersionSpecifier] dbt_version: List[VersionSpecifier]
@@ -628,7 +624,6 @@ class Project:
"sources": self.sources, "sources": self.sources,
"tests": self.tests, "tests": self.tests,
"metrics": self.metrics, "metrics": self.metrics,
"entities": self.entities,
"exposures": self.exposures, "exposures": self.exposures,
"vars": self.vars.to_dict(), "vars": self.vars.to_dict(),
"require-dbt-version": [v.to_version_string() for v in self.dbt_version], "require-dbt-version": [v.to_version_string() for v in self.dbt_version],
@@ -647,7 +642,7 @@ class Project:
try: try:
ProjectContract.validate(self.to_project_config()) ProjectContract.validate(self.to_project_config())
except ValidationError as e: except ValidationError as e:
raise ProjectContractBrokenError(e) from e raise DbtProjectError(validator_error_message(e)) from e
@classmethod @classmethod
def partial_load(cls, project_root: str, *, verify_version: bool = False) -> PartialProject: def partial_load(cls, project_root: str, *, verify_version: bool = False) -> PartialProject:
@@ -672,8 +667,8 @@ class Project:
def get_selector(self, name: str) -> Union[SelectionSpec, bool]: def get_selector(self, name: str) -> Union[SelectionSpec, bool]:
if name not in self.selectors: if name not in self.selectors:
raise DbtRuntimeError( raise RuntimeException(
f"Could not find selector named {name}, expected one of {list(self.selectors)}" f"Could not find selector named {name}, expected one of " f"{list(self.selectors)}"
) )
return self.selectors[name]["definition"] return self.selectors[name]["definition"]

View File

@@ -8,7 +8,7 @@ from dbt.context.target import TargetContext
from dbt.context.secret import SecretContext, SECRET_PLACEHOLDER from dbt.context.secret import SecretContext, SECRET_PLACEHOLDER
from dbt.context.base import BaseContext from dbt.context.base import BaseContext
from dbt.contracts.connection import HasCredentials from dbt.contracts.connection import HasCredentials
from dbt.exceptions import DbtProjectError, CompilationError, RecursionError from dbt.exceptions import DbtProjectError, CompilationException, RecursionException
from dbt.utils import deep_map_render from dbt.utils import deep_map_render
@@ -40,14 +40,14 @@ class BaseRenderer:
try: try:
with catch_jinja(): with catch_jinja():
return get_rendered(value, self.context, native=True) return get_rendered(value, self.context, native=True)
except CompilationError as exc: except CompilationException as exc:
msg = f"Could not render {value}: {exc.msg}" msg = f"Could not render {value}: {exc.msg}"
raise CompilationError(msg) from exc raise CompilationException(msg) from exc
def render_data(self, data: Dict[str, Any]) -> Dict[str, Any]: def render_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
try: try:
return deep_map_render(self.render_entry, data) return deep_map_render(self.render_entry, data)
except RecursionError: except RecursionException:
raise DbtProjectError( raise DbtProjectError(
f"Cycle detected: {self.name} input has a reference to itself", project=data f"Cycle detected: {self.name} input has a reference to itself", project=data
) )
@@ -159,8 +159,7 @@ class DbtProjectYamlRenderer(BaseRenderer):
if first in {"seeds", "models", "snapshots", "tests"}: if first in {"seeds", "models", "snapshots", "tests"}:
keypath_parts = {(k.lstrip("+ ") if isinstance(k, str) else k) for k in keypath} keypath_parts = {(k.lstrip("+ ") if isinstance(k, str) else k) for k in keypath}
# model-level hooks # model-level hooks
late_rendered_hooks = {"pre-hook", "post-hook", "pre_hook", "post_hook"} if "pre-hook" in keypath_parts or "post-hook" in keypath_parts:
if keypath_parts.intersection(late_rendered_hooks):
return False return False
return True return True

View File

@@ -3,42 +3,31 @@ import os
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import ( from typing import Dict, Any, Optional, Mapping, Iterator, Iterable, Tuple, List, MutableSet, Type
Any,
Dict,
Iterable,
Iterator,
Mapping,
MutableSet,
Optional,
Tuple,
Type,
Union,
)
from .profile import Profile
from .project import Project
from .renderer import DbtProjectYamlRenderer, ProfileRenderer
from .utils import parse_cli_vars
from dbt import flags from dbt import flags
from dbt.adapters.factory import get_include_paths, get_relation_class_by_name from dbt.adapters.factory import get_relation_class_by_name, get_include_paths
from dbt.helper_types import FQNPath, PathSet, DictDefaultEmptyStr
from dbt.config.profile import read_user_config from dbt.config.profile import read_user_config
from dbt.contracts.connection import AdapterRequiredConfig, Credentials from dbt.contracts.connection import AdapterRequiredConfig, Credentials
from dbt.contracts.graph.manifest import ManifestMetadata from dbt.contracts.graph.manifest import ManifestMetadata
from dbt.contracts.project import Configuration, UserConfig
from dbt.contracts.relation import ComponentName from dbt.contracts.relation import ComponentName
from dbt.dataclass_schema import ValidationError from dbt.ui import warning_tag
from dbt.exceptions import (
ConfigContractBrokenError,
DbtProjectError,
NonUniquePackageNameError,
DbtRuntimeError,
UninstalledPackagesFoundError,
)
from dbt.events.functions import warn_or_error
from dbt.events.types import UnusedResourceConfigPath
from dbt.helper_types import DictDefaultEmptyStr, FQNPath, PathSet
from .profile import Profile from dbt.contracts.project import Configuration, UserConfig
from .project import Project, PartialProject from dbt.exceptions import (
from .renderer import DbtProjectYamlRenderer, ProfileRenderer RuntimeException,
from .utils import parse_cli_vars DbtProjectError,
validator_error_message,
warn_or_error,
raise_compiler_error,
)
from dbt.dataclass_schema import ValidationError
def _project_quoting_dict(proj: Project, profile: Profile) -> Dict[ComponentName, bool]: def _project_quoting_dict(proj: Project, profile: Profile) -> Dict[ComponentName, bool]:
@@ -117,7 +106,6 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
sources=project.sources, sources=project.sources,
tests=project.tests, tests=project.tests,
metrics=project.metrics, metrics=project.metrics,
entities=project.entities,
exposures=project.exposures, exposures=project.exposures,
vars=project.vars, vars=project.vars,
config_version=project.config_version, config_version=project.config_version,
@@ -188,7 +176,7 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
try: try:
Configuration.validate(self.serialize()) Configuration.validate(self.serialize())
except ValidationError as e: except ValidationError as e:
raise ConfigContractBrokenError(e) from e raise DbtProjectError(validator_error_message(e)) from e
@classmethod @classmethod
def _get_rendered_profile( def _get_rendered_profile(
@@ -202,52 +190,28 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
@classmethod @classmethod
def collect_parts(cls: Type["RuntimeConfig"], args: Any) -> Tuple[Project, Profile]: def collect_parts(cls: Type["RuntimeConfig"], args: Any) -> Tuple[Project, Profile]:
# profile_name from the project
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, "vars", "{}"))
profile = cls.collect_profile(args=args)
project_renderer = DbtProjectYamlRenderer(profile, cli_vars)
project = cls.collect_project(args=args, project_renderer=project_renderer)
assert type(project) is Project
return (project, profile)
@classmethod
def collect_profile(
cls: Type["RuntimeConfig"], args: Any, profile_name: Optional[str] = None
) -> Profile:
cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, "vars", "{}"))
profile_renderer = ProfileRenderer(cli_vars)
# build the profile using the base renderer and the one fact we know
if profile_name is None:
# Note: only the named profile section is rendered here. The rest of the
# profile is ignored.
partial = cls.collect_project(args)
assert type(partial) is PartialProject
profile_name = partial.render_profile_name(profile_renderer)
profile = cls._get_rendered_profile(args, profile_renderer, profile_name)
# Save env_vars encountered in rendering for partial parsing
profile.profile_env_vars = profile_renderer.ctx_obj.env_vars
return profile
@classmethod
def collect_project(
cls: Type["RuntimeConfig"],
args: Any,
project_renderer: Optional[DbtProjectYamlRenderer] = None,
) -> Union[Project, PartialProject]:
project_root = args.project_dir if args.project_dir else os.getcwd() project_root = args.project_dir if args.project_dir else os.getcwd()
version_check = bool(flags.VERSION_CHECK) version_check = bool(flags.VERSION_CHECK)
partial = Project.partial_load(project_root, verify_version=version_check) partial = Project.partial_load(project_root, verify_version=version_check)
if project_renderer is None:
return partial # build the profile using the base renderer and the one fact we know
else: # Note: only the named profile section is rendered. The rest of the
project = partial.render(project_renderer) # profile is ignored.
project.project_env_vars = project_renderer.ctx_obj.env_vars cli_vars: Dict[str, Any] = parse_cli_vars(getattr(args, "vars", "{}"))
return project profile_renderer = ProfileRenderer(cli_vars)
profile_name = partial.render_profile_name(profile_renderer)
profile = cls._get_rendered_profile(args, profile_renderer, profile_name)
# Save env_vars encountered in rendering for partial parsing
profile.profile_env_vars = profile_renderer.ctx_obj.env_vars
# get a new renderer using our target information and render the
# project
project_renderer = DbtProjectYamlRenderer(profile, cli_vars)
project = partial.render(project_renderer)
# Save env_vars encountered in rendering for partial parsing
project.project_env_vars = project_renderer.ctx_obj.env_vars
return (project, profile)
# Called in main.py, lib.py, task/base.py # Called in main.py, lib.py, task/base.py
@classmethod @classmethod
@@ -259,7 +223,7 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
:param args: The arguments as parsed from the cli. :param args: The arguments as parsed from the cli.
:raises DbtProjectError: If the project is invalid or missing. :raises DbtProjectError: If the project is invalid or missing.
:raises DbtProfileError: If the profile is invalid or missing. :raises DbtProfileError: If the profile is invalid or missing.
:raises DbtValidationError: If the cli variables are invalid. :raises ValidationException: If the cli variables are invalid.
""" """
project, profile = cls.collect_parts(args) project, profile = cls.collect_parts(args)
@@ -313,15 +277,14 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
"sources": self._get_config_paths(self.sources), "sources": self._get_config_paths(self.sources),
"tests": self._get_config_paths(self.tests), "tests": self._get_config_paths(self.tests),
"metrics": self._get_config_paths(self.metrics), "metrics": self._get_config_paths(self.metrics),
"entities": self._get_config_paths(self.entities),
"exposures": self._get_config_paths(self.exposures), "exposures": self._get_config_paths(self.exposures),
} }
def warn_for_unused_resource_config_paths( def get_unused_resource_config_paths(
self, self,
resource_fqns: Mapping[str, PathSet], resource_fqns: Mapping[str, PathSet],
disabled: PathSet, disabled: PathSet,
) -> None: ) -> List[FQNPath]:
"""Return a list of lists of strings, where each inner list of strings """Return a list of lists of strings, where each inner list of strings
represents a type + FQN path of a resource configuration that is not represents a type + FQN path of a resource configuration that is not
used. used.
@@ -335,13 +298,23 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
for config_path in config_paths: for config_path in config_paths:
if not _is_config_used(config_path, fqns): if not _is_config_used(config_path, fqns):
resource_path = ".".join(i for i in ((resource_type,) + config_path)) unused_resource_config_paths.append((resource_type,) + config_path)
unused_resource_config_paths.append(resource_path) return unused_resource_config_paths
if len(unused_resource_config_paths) == 0: def warn_for_unused_resource_config_paths(
self,
resource_fqns: Mapping[str, PathSet],
disabled: PathSet,
) -> None:
unused = self.get_unused_resource_config_paths(resource_fqns, disabled)
if len(unused) == 0:
return return
warn_or_error(UnusedResourceConfigPath(unused_config_paths=unused_resource_config_paths)) msg = UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE.format(
len(unused), "\n".join("- {}".format(".".join(u)) for u in unused)
)
warn_or_error(msg, log_fmt=warning_tag("{}"))
def load_dependencies(self, base_only=False) -> Mapping[str, "RuntimeConfig"]: def load_dependencies(self, base_only=False) -> Mapping[str, "RuntimeConfig"]:
if self.dependencies is None: if self.dependencies is None:
@@ -355,15 +328,22 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
count_packages_specified = len(self.packages.packages) # type: ignore count_packages_specified = len(self.packages.packages) # type: ignore
count_packages_installed = len(tuple(self._get_project_directories())) count_packages_installed = len(tuple(self._get_project_directories()))
if count_packages_specified > count_packages_installed: if count_packages_specified > count_packages_installed:
raise UninstalledPackagesFoundError( raise_compiler_error(
count_packages_specified, f"dbt found {count_packages_specified} package(s) "
count_packages_installed, f"specified in packages.yml, but only "
self.packages_install_path, f"{count_packages_installed} package(s) installed "
f'in {self.packages_install_path}. Run "dbt deps" to '
f"install package dependencies."
) )
project_paths = itertools.chain(internal_packages, self._get_project_directories()) project_paths = itertools.chain(internal_packages, self._get_project_directories())
for project_name, project in self.load_projects(project_paths): for project_name, project in self.load_projects(project_paths):
if project_name in all_projects: if project_name in all_projects:
raise NonUniquePackageNameError(project_name) raise_compiler_error(
f"dbt found more than one package with the name "
f'"{project_name}" included in this project. Package '
f"names must be unique in a project. Please rename "
f"one of these packages."
)
all_projects[project_name] = project all_projects[project_name] = project
self.dependencies = all_projects self.dependencies = all_projects
return self.dependencies return self.dependencies
@@ -428,7 +408,7 @@ class UnsetProfile(Profile):
def __getattribute__(self, name): def __getattribute__(self, name):
if name in {"profile_name", "target_name", "threads"}: if name in {"profile_name", "target_name", "threads"}:
raise DbtRuntimeError(f'Error: disallowed attribute "{name}" - no profile!') raise RuntimeException(f'Error: disallowed attribute "{name}" - no profile!')
return Profile.__getattribute__(self, name) return Profile.__getattribute__(self, name)
@@ -455,7 +435,7 @@ class UnsetProfileConfig(RuntimeConfig):
def __getattribute__(self, name): def __getattribute__(self, name):
# Override __getattribute__ to check that the attribute isn't 'banned'. # Override __getattribute__ to check that the attribute isn't 'banned'.
if name in {"profile_name", "target_name"}: if name in {"profile_name", "target_name"}:
raise DbtRuntimeError(f'Error: disallowed attribute "{name}" - no profile!') raise RuntimeException(f'Error: disallowed attribute "{name}" - no profile!')
# avoid every attribute access triggering infinite recursion # avoid every attribute access triggering infinite recursion
return RuntimeConfig.__getattribute__(self, name) return RuntimeConfig.__getattribute__(self, name)
@@ -502,7 +482,6 @@ class UnsetProfileConfig(RuntimeConfig):
"sources": self.sources, "sources": self.sources,
"tests": self.tests, "tests": self.tests,
"metrics": self.metrics, "metrics": self.metrics,
"entities": self.entities,
"exposures": self.exposures, "exposures": self.exposures,
"vars": self.vars.to_dict(), "vars": self.vars.to_dict(),
"require-dbt-version": [v.to_version_string() for v in self.dbt_version], "require-dbt-version": [v.to_version_string() for v in self.dbt_version],
@@ -565,7 +544,6 @@ class UnsetProfileConfig(RuntimeConfig):
sources=project.sources, sources=project.sources,
tests=project.tests, tests=project.tests,
metrics=project.metrics, metrics=project.metrics,
entities=project.entities,
exposures=project.exposures, exposures=project.exposures,
vars=project.vars, vars=project.vars,
config_version=project.config_version, config_version=project.config_version,
@@ -606,13 +584,21 @@ class UnsetProfileConfig(RuntimeConfig):
:param args: The arguments as parsed from the cli. :param args: The arguments as parsed from the cli.
:raises DbtProjectError: If the project is invalid or missing. :raises DbtProjectError: If the project is invalid or missing.
:raises DbtProfileError: If the profile is invalid or missing. :raises DbtProfileError: If the profile is invalid or missing.
:raises DbtValidationError: If the cli variables are invalid. :raises ValidationException: If the cli variables are invalid.
""" """
project, profile = cls.collect_parts(args) project, profile = cls.collect_parts(args)
return cls.from_parts(project=project, profile=profile, args=args) return cls.from_parts(project=project, profile=profile, args=args)
UNUSED_RESOURCE_CONFIGURATION_PATH_MESSAGE = """\
Configuration paths exist in your dbt_project.yml file which do not \
apply to any resources.
There are {} unused configuration paths:
{}
"""
def _is_config_used(path, fqns): def _is_config_used(path, fqns):
if fqns: if fqns:
for fqn in fqns: for fqn in fqns:

View File

@@ -12,7 +12,7 @@ from dbt.clients.system import (
resolve_path_from_base, resolve_path_from_base,
) )
from dbt.contracts.selection import SelectorFile from dbt.contracts.selection import SelectorFile
from dbt.exceptions import DbtSelectorsError, DbtRuntimeError from dbt.exceptions import DbtSelectorsError, RuntimeException
from dbt.graph import parse_from_selectors_definition, SelectionSpec from dbt.graph import parse_from_selectors_definition, SelectionSpec
from dbt.graph.selector_spec import SelectionCriteria from dbt.graph.selector_spec import SelectionCriteria
@@ -46,7 +46,7 @@ class SelectorConfig(Dict[str, Dict[str, Union[SelectionSpec, bool]]]):
f"yaml-selectors", f"yaml-selectors",
result_type="invalid_selector", result_type="invalid_selector",
) from exc ) from exc
except DbtRuntimeError as exc: except RuntimeException as exc:
raise DbtSelectorsError( raise DbtSelectorsError(
f"Could not read selector file data: {exc}", f"Could not read selector file data: {exc}",
result_type="invalid_selector", result_type="invalid_selector",
@@ -62,7 +62,7 @@ class SelectorConfig(Dict[str, Dict[str, Union[SelectionSpec, bool]]]):
) -> "SelectorConfig": ) -> "SelectorConfig":
try: try:
rendered = renderer.render_data(data) rendered = renderer.render_data(data)
except (ValidationError, DbtRuntimeError) as exc: except (ValidationError, RuntimeException) as exc:
raise DbtSelectorsError( raise DbtSelectorsError(
f"Could not render selector data: {exc}", f"Could not render selector data: {exc}",
result_type="invalid_selector", result_type="invalid_selector",
@@ -77,7 +77,7 @@ class SelectorConfig(Dict[str, Dict[str, Union[SelectionSpec, bool]]]):
) -> "SelectorConfig": ) -> "SelectorConfig":
try: try:
data = load_yaml_text(load_file_contents(str(path))) data = load_yaml_text(load_file_contents(str(path)))
except (ValidationError, DbtRuntimeError) as exc: except (ValidationError, RuntimeException) as exc:
raise DbtSelectorsError( raise DbtSelectorsError(
f"Could not read selector file: {exc}", f"Could not read selector file: {exc}",
result_type="invalid_selector", result_type="invalid_selector",

View File

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

View File

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

View File

@@ -4,22 +4,19 @@ from typing import Any, Dict, NoReturn, Optional, Mapping, Iterable, Set, List
from dbt import flags from dbt import flags
from dbt import tracking from dbt import tracking
from dbt import utils
from dbt.clients.jinja import get_rendered from dbt.clients.jinja import get_rendered
from dbt.clients.yaml_helper import yaml, safe_load, SafeLoader, Loader, Dumper # noqa: F401 from dbt.clients.yaml_helper import yaml, safe_load, SafeLoader, Loader, Dumper # noqa: F401
from dbt.constants import SECRET_ENV_PREFIX, DEFAULT_ENV_PLACEHOLDER from dbt.constants import SECRET_ENV_PREFIX, DEFAULT_ENV_PLACEHOLDER
from dbt.contracts.graph.nodes import Resource from dbt.contracts.graph.compiled import CompiledResource
from dbt.exceptions import ( from dbt.exceptions import (
SecretEnvVarLocationError, CompilationException,
EnvVarMissingError,
MacroReturn, MacroReturn,
RequiredVarNotFoundError, raise_compiler_error,
SetStrictWrongTypeError, raise_parsing_error,
ZipStrictWrongTypeError, disallow_secret_env_var,
) )
from dbt.events.functions import fire_event, get_invocation_id from dbt.events.functions import fire_event, get_invocation_id
from dbt.events.types import JinjaLogInfo, JinjaLogDebug from dbt.events.types import MacroEventInfo, MacroEventDebug
from dbt.events.contextvars import get_node_info
from dbt.version import __version__ as dbt_version from dbt.version import __version__ as dbt_version
# These modules are added to the context. Consider alternative # These modules are added to the context. Consider alternative
@@ -129,17 +126,18 @@ class ContextMeta(type):
class Var: class Var:
UndefinedVarError = "Required var '{}' not found in config:\nVars " "supplied to {} = {}"
_VAR_NOTSET = object() _VAR_NOTSET = object()
def __init__( def __init__(
self, self,
context: Mapping[str, Any], context: Mapping[str, Any],
cli_vars: Mapping[str, Any], cli_vars: Mapping[str, Any],
node: Optional[Resource] = None, node: Optional[CompiledResource] = None,
) -> None: ) -> None:
self._context: Mapping[str, Any] = context self._context: Mapping[str, Any] = context
self._cli_vars: Mapping[str, Any] = cli_vars self._cli_vars: Mapping[str, Any] = cli_vars
self._node: Optional[Resource] = node self._node: Optional[CompiledResource] = node
self._merged: Mapping[str, Any] = self._generate_merged() self._merged: Mapping[str, Any] = self._generate_merged()
def _generate_merged(self) -> Mapping[str, Any]: def _generate_merged(self) -> Mapping[str, Any]:
@@ -153,7 +151,10 @@ class Var:
return "<Configuration>" return "<Configuration>"
def get_missing_var(self, var_name): def get_missing_var(self, var_name):
raise RequiredVarNotFoundError(var_name, self._merged, self._node) dct = {k: self._merged[k] for k in self._merged}
pretty_vars = json.dumps(dct, sort_keys=True, indent=4)
msg = self.UndefinedVarError.format(var_name, self.node_name, pretty_vars)
raise_compiler_error(msg, self._node)
def has_var(self, var_name: str): def has_var(self, var_name: str):
return var_name in self._merged return var_name in self._merged
@@ -297,7 +298,7 @@ class BaseContext(metaclass=ContextMeta):
""" """
return_value = None return_value = None
if var.startswith(SECRET_ENV_PREFIX): if var.startswith(SECRET_ENV_PREFIX):
raise SecretEnvVarLocationError(var) disallow_secret_env_var(var)
if var in os.environ: if var in os.environ:
return_value = os.environ[var] return_value = os.environ[var]
elif default is not None: elif default is not None:
@@ -312,7 +313,8 @@ class BaseContext(metaclass=ContextMeta):
return return_value return return_value
else: else:
raise EnvVarMissingError(var) msg = f"Env var required but not provided: '{var}'"
raise_parsing_error(msg)
if os.environ.get("DBT_MACRO_DEBUGGING"): if os.environ.get("DBT_MACRO_DEBUGGING"):
@@ -493,7 +495,7 @@ class BaseContext(metaclass=ContextMeta):
try: try:
return set(value) return set(value)
except TypeError as e: except TypeError as e:
raise SetStrictWrongTypeError(e) raise CompilationException(e)
@contextmember("zip") @contextmember("zip")
@staticmethod @staticmethod
@@ -537,7 +539,7 @@ class BaseContext(metaclass=ContextMeta):
try: try:
return zip(*args) return zip(*args)
except TypeError as e: except TypeError as e:
raise ZipStrictWrongTypeError(e) raise CompilationException(e)
@contextmember @contextmember
@staticmethod @staticmethod
@@ -555,9 +557,9 @@ class BaseContext(metaclass=ContextMeta):
{% endmacro %}" {% endmacro %}"
""" """
if info: if info:
fire_event(JinjaLogInfo(msg=msg, node_info=get_node_info())) fire_event(MacroEventInfo(msg=msg))
else: else:
fire_event(JinjaLogDebug(msg=msg, node_info=get_node_info())) fire_event(MacroEventDebug(msg=msg))
return "" return ""
@contextproperty @contextproperty
@@ -634,8 +636,9 @@ class BaseContext(metaclass=ContextMeta):
{% endif %} {% endif %}
This supports all flags defined in flags submodule (core/dbt/flags.py) This supports all flags defined in flags submodule (core/dbt/flags.py)
TODO: Replace with object that provides read-only access to flag values
""" """
return flags.get_flag_obj() return flags
@contextmember @contextmember
@staticmethod @staticmethod
@@ -684,19 +687,6 @@ class BaseContext(metaclass=ContextMeta):
dict_diff.update({k: dict_a[k]}) dict_diff.update({k: dict_a[k]})
return dict_diff return dict_diff
@contextmember
@staticmethod
def local_md5(value: str) -> str:
"""Calculates an MD5 hash of the given string.
It's called "local_md5" to emphasize that it runs locally in dbt (in jinja context) and not an MD5 SQL command.
:param value: The value to hash
Usage:
{% set value_hash = local_md5("hello world") %}
"""
return utils.md5(value)
def generate_base_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]: def generate_base_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]:
ctx = BaseContext(cli_vars) ctx = BaseContext(cli_vars)

View File

@@ -8,7 +8,7 @@ from dbt.utils import MultiDict
from dbt.context.base import contextproperty, contextmember, Var from dbt.context.base import contextproperty, contextmember, Var
from dbt.context.target import TargetContext from dbt.context.target import TargetContext
from dbt.exceptions import EnvVarMissingError, SecretEnvVarLocationError from dbt.exceptions import raise_parsing_error, disallow_secret_env_var
class ConfiguredContext(TargetContext): class ConfiguredContext(TargetContext):
@@ -86,7 +86,7 @@ class SchemaYamlContext(ConfiguredContext):
def env_var(self, var: str, default: Optional[str] = None) -> str: def env_var(self, var: str, default: Optional[str] = None) -> str:
return_value = None return_value = None
if var.startswith(SECRET_ENV_PREFIX): if var.startswith(SECRET_ENV_PREFIX):
raise SecretEnvVarLocationError(var) disallow_secret_env_var(var)
if var in os.environ: if var in os.environ:
return_value = os.environ[var] return_value = os.environ[var]
elif default is not None: elif default is not None:
@@ -104,7 +104,8 @@ class SchemaYamlContext(ConfiguredContext):
return return_value return return_value
else: else:
raise EnvVarMissingError(var) msg = f"Env var required but not provided: '{var}'"
raise_parsing_error(msg)
class MacroResolvingContext(ConfiguredContext): class MacroResolvingContext(ConfiguredContext):

View File

@@ -5,7 +5,7 @@ from typing import List, Iterator, Dict, Any, TypeVar, Generic
from dbt.config import RuntimeConfig, Project, IsFQNResource from dbt.config import RuntimeConfig, Project, IsFQNResource
from dbt.contracts.graph.model_config import BaseConfig, get_config_for, _listify from dbt.contracts.graph.model_config import BaseConfig, get_config_for, _listify
from dbt.exceptions import DbtInternalError from dbt.exceptions import InternalException
from dbt.node_types import NodeType from dbt.node_types import NodeType
from dbt.utils import fqn_search from dbt.utils import fqn_search
@@ -45,8 +45,6 @@ class UnrenderedConfig(ConfigSource):
model_configs = unrendered.get("tests") model_configs = unrendered.get("tests")
elif resource_type == NodeType.Metric: elif resource_type == NodeType.Metric:
model_configs = unrendered.get("metrics") model_configs = unrendered.get("metrics")
elif resource_type == NodeType.Entity:
model_configs = unrendered.get("entities")
elif resource_type == NodeType.Exposure: elif resource_type == NodeType.Exposure:
model_configs = unrendered.get("exposures") model_configs = unrendered.get("exposures")
else: else:
@@ -72,8 +70,6 @@ class RenderedConfig(ConfigSource):
model_configs = self.project.tests model_configs = self.project.tests
elif resource_type == NodeType.Metric: elif resource_type == NodeType.Metric:
model_configs = self.project.metrics model_configs = self.project.metrics
elif resource_type == NodeType.Entity:
model_configs = self.project.entities
elif resource_type == NodeType.Exposure: elif resource_type == NodeType.Exposure:
model_configs = self.project.exposures model_configs = self.project.exposures
else: else:
@@ -93,7 +89,7 @@ class BaseContextConfigGenerator(Generic[T]):
return self._active_project return self._active_project
dependencies = self._active_project.load_dependencies() dependencies = self._active_project.load_dependencies()
if project_name not in dependencies: if project_name not in dependencies:
raise DbtInternalError( raise InternalException(
f"Project name {project_name} not found in dependencies " f"Project name {project_name} not found in dependencies "
f"(found {list(dependencies)})" f"(found {list(dependencies)})"
) )
@@ -291,14 +287,14 @@ class ContextConfig:
elif k in BaseConfig.mergebehavior["update"]: elif k in BaseConfig.mergebehavior["update"]:
if not isinstance(v, dict): if not isinstance(v, dict):
raise DbtInternalError(f"expected dict, got {v}") raise InternalException(f"expected dict, got {v}")
if k in config_call_dict and isinstance(config_call_dict[k], dict): if k in config_call_dict and isinstance(config_call_dict[k], dict):
config_call_dict[k].update(v) config_call_dict[k].update(v)
else: else:
config_call_dict[k] = v config_call_dict[k] = v
elif k in BaseConfig.mergebehavior["dict_key_append"]: elif k in BaseConfig.mergebehavior["dict_key_append"]:
if not isinstance(v, dict): if not isinstance(v, dict):
raise DbtInternalError(f"expected dict, got {v}") raise InternalException(f"expected dict, got {v}")
if k in config_call_dict: # should always be a dict if k in config_call_dict: # should always be a dict
for key, value in v.items(): for key, value in v.items():
extend = False extend = False

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
from typing import Dict, MutableMapping, Optional from typing import Dict, MutableMapping, Optional
from dbt.contracts.graph.nodes import Macro from dbt.contracts.graph.parsed import ParsedMacro
from dbt.exceptions import DuplicateMacroNameError, PackageNotFoundForMacroError from dbt.exceptions import raise_duplicate_macro_name, raise_compiler_error
from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME from dbt.include.global_project import PROJECT_NAME as GLOBAL_PROJECT_NAME
from dbt.clients.jinja import MacroGenerator from dbt.clients.jinja import MacroGenerator
MacroNamespace = Dict[str, Macro] MacroNamespace = Dict[str, ParsedMacro]
# This class builds the MacroResolver by adding macros # This class builds the MacroResolver by adding macros
@@ -21,7 +21,7 @@ MacroNamespace = Dict[str, Macro]
class MacroResolver: class MacroResolver:
def __init__( def __init__(
self, self,
macros: MutableMapping[str, Macro], macros: MutableMapping[str, ParsedMacro],
root_project_name: str, root_project_name: str,
internal_package_names, internal_package_names,
) -> None: ) -> None:
@@ -77,7 +77,7 @@ class MacroResolver:
def _add_macro_to( def _add_macro_to(
self, self,
package_namespaces: Dict[str, MacroNamespace], package_namespaces: Dict[str, MacroNamespace],
macro: Macro, macro: ParsedMacro,
): ):
if macro.package_name in package_namespaces: if macro.package_name in package_namespaces:
namespace = package_namespaces[macro.package_name] namespace = package_namespaces[macro.package_name]
@@ -86,10 +86,10 @@ class MacroResolver:
package_namespaces[macro.package_name] = namespace package_namespaces[macro.package_name] = namespace
if macro.name in namespace: if macro.name in namespace:
raise DuplicateMacroNameError(macro, macro, macro.package_name) raise_duplicate_macro_name(macro, macro, macro.package_name)
package_namespaces[macro.package_name][macro.name] = macro package_namespaces[macro.package_name][macro.name] = macro
def add_macro(self, macro: Macro): def add_macro(self, macro: ParsedMacro):
macro_name: str = macro.name macro_name: str = macro.name
# internal macros (from plugins) will be processed separately from # internal macros (from plugins) will be processed separately from
@@ -187,7 +187,7 @@ class TestMacroNamespace:
elif package_name in self.macro_resolver.packages: elif package_name in self.macro_resolver.packages:
macro = self.macro_resolver.packages[package_name].get(name) macro = self.macro_resolver.packages[package_name].get(name)
else: else:
raise PackageNotFoundForMacroError(package_name) raise_compiler_error(f"Could not find package '{package_name}'")
if not macro: if not macro:
return None return None
macro_func = MacroGenerator(macro, self.ctx, self.node, self.thread_ctx) macro_func = MacroGenerator(macro, self.ctx, self.node, self.thread_ctx)

View File

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

View File

@@ -19,51 +19,46 @@ from dbt.adapters.factory import get_adapter, get_adapter_package_names, get_ada
from dbt.clients import agate_helper from dbt.clients import agate_helper
from dbt.clients.jinja import get_rendered, MacroGenerator, MacroStack from dbt.clients.jinja import get_rendered, MacroGenerator, MacroStack
from dbt.config import RuntimeConfig, Project from dbt.config import RuntimeConfig, Project
from .base import contextmember, contextproperty, Var
from .configured import FQNLookup
from .context_config import ContextConfig
from dbt.constants import SECRET_ENV_PREFIX, DEFAULT_ENV_PLACEHOLDER from dbt.constants import SECRET_ENV_PREFIX, DEFAULT_ENV_PLACEHOLDER
from dbt.context.base import contextmember, contextproperty, Var
from dbt.context.configured import FQNLookup
from dbt.context.context_config import ContextConfig
from dbt.context.exceptions_jinja import wrapped_exports
from dbt.context.macro_resolver import MacroResolver, TestMacroNamespace from dbt.context.macro_resolver import MacroResolver, TestMacroNamespace
from dbt.context.macros import MacroNamespaceBuilder, MacroNamespace from .macros import MacroNamespaceBuilder, MacroNamespace
from dbt.context.manifest import ManifestContext from .manifest import ManifestContext
from dbt.contracts.connection import AdapterResponse from dbt.contracts.connection import AdapterResponse
from dbt.contracts.graph.manifest import Manifest, Disabled from dbt.contracts.graph.manifest import Manifest, Disabled
from dbt.contracts.graph.nodes import ( from dbt.contracts.graph.compiled import (
Macro, CompiledResource,
Exposure, CompiledSeedNode,
Metric,
Entity,
SeedNode,
SourceDefinition,
Resource,
ManifestNode, ManifestNode,
) )
from dbt.contracts.graph.parsed import (
ParsedMacro,
ParsedExposure,
ParsedMetric,
ParsedSeedNode,
ParsedSourceDefinition,
)
from dbt.contracts.graph.metrics import MetricReference, ResolvedMetricReference from dbt.contracts.graph.metrics import MetricReference, ResolvedMetricReference
from dbt.events.functions import get_metadata_vars from dbt.contracts.util import get_metadata_env
from dbt.exceptions import ( from dbt.exceptions import (
CompilationError, CompilationException,
ConflictingConfigKeysError, ParsingException,
SecretEnvVarLocationError, InternalException,
EnvVarMissingError, ValidationException,
DbtInternalError, RuntimeException,
InlineModelConfigError, macro_invalid_dispatch_arg,
NumberSourceArgsError, missing_config,
PersistDocsValueTypeError, raise_compiler_error,
LoadAgateTableNotSeedError, ref_invalid_args,
LoadAgateTableValueError, metric_invalid_args,
MacroDispatchArgError, ref_target_not_found,
MacrosSourcesUnWriteableError, target_not_found,
MetricArgsError, ref_bad_context,
MissingConfigError, wrapped_exports,
OperationsCannotRefEphemeralNodesError, raise_parsing_error,
PackageNotInDepsError, disallow_secret_env_var,
ParsingError,
RefBadContextError,
RefArgsError,
DbtRuntimeError,
TargetNotFoundError,
DbtValidationError,
) )
from dbt.config import IsFQNResource from dbt.config import IsFQNResource
from dbt.node_types import NodeType, ModelLanguage from dbt.node_types import NodeType, ModelLanguage
@@ -145,10 +140,10 @@ class BaseDatabaseWrapper:
f'`adapter.dispatch("{suggest_macro_name}", ' f'`adapter.dispatch("{suggest_macro_name}", '
f'macro_namespace="{suggest_macro_namespace}")`?' f'macro_namespace="{suggest_macro_namespace}")`?'
) )
raise CompilationError(msg) raise CompilationException(msg)
if packages is not None: if packages is not None:
raise MacroDispatchArgError(macro_name) raise macro_invalid_dispatch_arg(macro_name)
namespace = macro_namespace namespace = macro_namespace
@@ -160,7 +155,7 @@ class BaseDatabaseWrapper:
search_packages = [self.config.project_name, namespace] search_packages = [self.config.project_name, namespace]
else: else:
# Not a string and not None so must be a list # Not a string and not None so must be a list
raise CompilationError( raise CompilationException(
f"In adapter.dispatch, got a list macro_namespace argument " f"In adapter.dispatch, got a list macro_namespace argument "
f'("{macro_namespace}"), but macro_namespace should be None or a string.' f'("{macro_namespace}"), but macro_namespace should be None or a string.'
) )
@@ -173,8 +168,8 @@ class BaseDatabaseWrapper:
try: try:
# this uses the namespace from the context # this uses the namespace from the context
macro = self._namespace.get_from_package(package_name, search_name) macro = self._namespace.get_from_package(package_name, search_name)
except CompilationError: except CompilationException:
# Only raise CompilationError if macro is not found in # Only raise CompilationException if macro is not found in
# any package # any package
macro = None macro = None
@@ -187,8 +182,8 @@ class BaseDatabaseWrapper:
return macro return macro
searched = ", ".join(repr(a) for a in attempts) searched = ", ".join(repr(a) for a in attempts)
msg = f"In dispatch: No macro named '{macro_name}' found\n Searched for: {searched}" msg = f"In dispatch: No macro named '{macro_name}' found\n" f" Searched for: {searched}"
raise CompilationError(msg) raise CompilationException(msg)
class BaseResolver(metaclass=abc.ABCMeta): class BaseResolver(metaclass=abc.ABCMeta):
@@ -224,13 +219,13 @@ class BaseRefResolver(BaseResolver):
def validate_args(self, name: str, package: Optional[str]): def validate_args(self, name: str, package: Optional[str]):
if not isinstance(name, str): if not isinstance(name, str):
raise CompilationError( raise CompilationException(
f"The name argument to ref() must be a string, got {type(name)}" f"The name argument to ref() must be a string, got " f"{type(name)}"
) )
if package is not None and not isinstance(package, str): if package is not None and not isinstance(package, str):
raise CompilationError( raise CompilationException(
f"The package argument to ref() must be a string or None, got {type(package)}" f"The package argument to ref() must be a string or None, got " f"{type(package)}"
) )
def __call__(self, *args: str) -> RelationProxy: def __call__(self, *args: str) -> RelationProxy:
@@ -242,7 +237,7 @@ class BaseRefResolver(BaseResolver):
elif len(args) == 2: elif len(args) == 2:
package, name = args package, name = args
else: else:
raise RefArgsError(node=self.model, args=args) ref_invalid_args(self.model, args)
self.validate_args(name, package) self.validate_args(name, package)
return self.resolve(name, package) return self.resolve(name, package)
@@ -254,19 +249,21 @@ class BaseSourceResolver(BaseResolver):
def validate_args(self, source_name: str, table_name: str): def validate_args(self, source_name: str, table_name: str):
if not isinstance(source_name, str): if not isinstance(source_name, str):
raise CompilationError( raise CompilationException(
f"The source name (first) argument to source() must be a " f"The source name (first) argument to source() must be a "
f"string, got {type(source_name)}" f"string, got {type(source_name)}"
) )
if not isinstance(table_name, str): if not isinstance(table_name, str):
raise CompilationError( raise CompilationException(
f"The table name (second) argument to source() must be a " f"The table name (second) argument to source() must be a "
f"string, got {type(table_name)}" f"string, got {type(table_name)}"
) )
def __call__(self, *args: str) -> RelationProxy: def __call__(self, *args: str) -> RelationProxy:
if len(args) != 2: if len(args) != 2:
raise NumberSourceArgsError(args, node=self.model) raise_compiler_error(
f"source() takes exactly two arguments ({len(args)} given)", self.model
)
self.validate_args(args[0], args[1]) self.validate_args(args[0], args[1])
return self.resolve(args[0], args[1]) return self.resolve(args[0], args[1])
@@ -283,12 +280,12 @@ class BaseMetricResolver(BaseResolver):
def validate_args(self, name: str, package: Optional[str]): def validate_args(self, name: str, package: Optional[str]):
if not isinstance(name, str): if not isinstance(name, str):
raise CompilationError( raise CompilationException(
f"The name argument to metric() must be a string, got {type(name)}" f"The name argument to metric() must be a string, got {type(name)}"
) )
if package is not None and not isinstance(package, str): if package is not None and not isinstance(package, str):
raise CompilationError( raise CompilationException(
f"The package argument to metric() must be a string or None, got {type(package)}" f"The package argument to metric() must be a string or None, got {type(package)}"
) )
@@ -301,7 +298,7 @@ class BaseMetricResolver(BaseResolver):
elif len(args) == 2: elif len(args) == 2:
package, name = args package, name = args
else: else:
raise MetricArgsError(node=self.model, args=args) metric_invalid_args(self.model, args)
self.validate_args(name, package) self.validate_args(name, package)
return self.resolve(name, package) return self.resolve(name, package)
@@ -322,7 +319,12 @@ class ParseConfigObject(Config):
if oldkey in config: if oldkey in config:
newkey = oldkey.replace("_", "-") newkey = oldkey.replace("_", "-")
if newkey in config: if newkey in config:
raise ConflictingConfigKeysError(oldkey, newkey, node=self.model) raise_compiler_error(
'Invalid config, has conflicting keys "{}" and "{}"'.format(
oldkey, newkey
),
self.model,
)
config[newkey] = config.pop(oldkey) config[newkey] = config.pop(oldkey)
return config return config
@@ -332,14 +334,14 @@ class ParseConfigObject(Config):
elif len(args) == 0 and len(kwargs) > 0: elif len(args) == 0 and len(kwargs) > 0:
opts = kwargs opts = kwargs
else: else:
raise InlineModelConfigError(node=self.model) raise_compiler_error("Invalid inline model config", self.model)
opts = self._transform_config(opts) opts = self._transform_config(opts)
# it's ok to have a parse context with no context config, but you must # it's ok to have a parse context with no context config, but you must
# not call it! # not call it!
if self.context_config is None: if self.context_config is None:
raise DbtRuntimeError("At parse time, did not receive a context config") raise RuntimeException("At parse time, did not receive a context config")
self.context_config.add_config_call(opts) self.context_config.add_config_call(opts)
return "" return ""
@@ -380,7 +382,7 @@ class RuntimeConfigObject(Config):
else: else:
result = self.model.config.get(name, default) result = self.model.config.get(name, default)
if result is _MISSING: if result is _MISSING:
raise MissingConfigError(unique_id=self.model.unique_id, name=name) missing_config(self.model, name)
return result return result
def require(self, name, validator=None): def require(self, name, validator=None):
@@ -402,14 +404,20 @@ class RuntimeConfigObject(Config):
def persist_relation_docs(self) -> bool: def persist_relation_docs(self) -> bool:
persist_docs = self.get("persist_docs", default={}) persist_docs = self.get("persist_docs", default={})
if not isinstance(persist_docs, dict): if not isinstance(persist_docs, dict):
raise PersistDocsValueTypeError(persist_docs) raise_compiler_error(
f"Invalid value provided for 'persist_docs'. Expected dict "
f"but received {type(persist_docs)}"
)
return persist_docs.get("relation", False) return persist_docs.get("relation", False)
def persist_column_docs(self) -> bool: def persist_column_docs(self) -> bool:
persist_docs = self.get("persist_docs", default={}) persist_docs = self.get("persist_docs", default={})
if not isinstance(persist_docs, dict): if not isinstance(persist_docs, dict):
raise PersistDocsValueTypeError(persist_docs) raise_compiler_error(
f"Invalid value provided for 'persist_docs'. Expected dict "
f"but received {type(persist_docs)}"
)
return persist_docs.get("columns", False) return persist_docs.get("columns", False)
@@ -468,11 +476,10 @@ class RuntimeRefResolver(BaseRefResolver):
) )
if target_model is None or isinstance(target_model, Disabled): if target_model is None or isinstance(target_model, Disabled):
raise TargetNotFoundError( ref_target_not_found(
node=self.model, self.model,
target_name=target_name, target_name,
target_kind="node", target_package,
target_package=target_package,
disabled=isinstance(target_model, Disabled), disabled=isinstance(target_model, Disabled),
) )
self.validate(target_model, target_name, target_package) self.validate(target_model, target_name, target_package)
@@ -490,7 +497,7 @@ class RuntimeRefResolver(BaseRefResolver):
) -> None: ) -> None:
if resolved.unique_id not in self.model.depends_on.nodes: if resolved.unique_id not in self.model.depends_on.nodes:
args = self._repack_args(target_name, target_package) args = self._repack_args(target_name, target_package)
raise RefBadContextError(node=self.model, args=args) ref_bad_context(self.model, args)
class OperationRefResolver(RuntimeRefResolver): class OperationRefResolver(RuntimeRefResolver):
@@ -505,8 +512,13 @@ class OperationRefResolver(RuntimeRefResolver):
def create_relation(self, target_model: ManifestNode, name: str) -> RelationProxy: def create_relation(self, target_model: ManifestNode, name: str) -> RelationProxy:
if target_model.is_ephemeral_model: if target_model.is_ephemeral_model:
# In operations, we can't ref() ephemeral nodes, because # In operations, we can't ref() ephemeral nodes, because
# Macros do not support set_cte # ParsedMacros do not support set_cte
raise OperationsCannotRefEphemeralNodesError(target_model.name, node=self.model) raise_compiler_error(
"Operations can not ref() ephemeral nodes, but {} is ephemeral".format(
target_model.name
),
self.model,
)
else: else:
return super().create_relation(target_model, name) return super().create_relation(target_model, name)
@@ -529,7 +541,7 @@ class RuntimeSourceResolver(BaseSourceResolver):
) )
if target_source is None or isinstance(target_source, Disabled): if target_source is None or isinstance(target_source, Disabled):
raise TargetNotFoundError( target_not_found(
node=self.model, node=self.model,
target_name=f"{source_name}.{table_name}", target_name=f"{source_name}.{table_name}",
target_kind="source", target_kind="source",
@@ -556,7 +568,7 @@ class RuntimeMetricResolver(BaseMetricResolver):
) )
if target_metric is None or isinstance(target_metric, Disabled): if target_metric is None or isinstance(target_metric, Disabled):
raise TargetNotFoundError( target_not_found(
node=self.model, node=self.model,
target_name=target_name, target_name=target_name,
target_kind="metric", target_kind="metric",
@@ -572,9 +584,9 @@ class ModelConfiguredVar(Var):
self, self,
context: Dict[str, Any], context: Dict[str, Any],
config: RuntimeConfig, config: RuntimeConfig,
node: Resource, node: CompiledResource,
) -> None: ) -> None:
self._node: Resource self._node: CompiledResource
self._config: RuntimeConfig = config self._config: RuntimeConfig = config
super().__init__(context, config.cli_vars, node=node) super().__init__(context, config.cli_vars, node=node)
@@ -585,7 +597,7 @@ class ModelConfiguredVar(Var):
if package_name != self._config.project_name: if package_name != self._config.project_name:
if package_name not in dependencies: if package_name not in dependencies:
# I don't think this is actually reachable # I don't think this is actually reachable
raise PackageNotInDepsError(package_name, node=self._node) raise_compiler_error(f"Node package named {package_name} not found!", self._node)
yield dependencies[package_name] yield dependencies[package_name]
yield self._config yield self._config
@@ -675,10 +687,10 @@ class ProviderContext(ManifestContext):
context_config: Optional[ContextConfig], context_config: Optional[ContextConfig],
) -> None: ) -> None:
if provider is None: if provider is None:
raise DbtInternalError(f"Invalid provider given to context: {provider}") raise InternalException(f"Invalid provider given to context: {provider}")
# mypy appeasement - we know it'll be a RuntimeConfig # mypy appeasement - we know it'll be a RuntimeConfig
self.config: RuntimeConfig self.config: RuntimeConfig
self.model: Union[Macro, ManifestNode] = model self.model: Union[ParsedMacro, ManifestNode] = model
super().__init__(config, manifest, model.package_name) super().__init__(config, manifest, model.package_name)
self.sql_results: Dict[str, AttrDict] = {} self.sql_results: Dict[str, AttrDict] = {}
self.context_config: Optional[ContextConfig] = context_config self.context_config: Optional[ContextConfig] = context_config
@@ -701,7 +713,7 @@ class ProviderContext(ManifestContext):
@contextproperty @contextproperty
def dbt_metadata_envs(self) -> Dict[str, str]: def dbt_metadata_envs(self) -> Dict[str, str]:
return get_metadata_vars() return get_metadata_env()
@contextproperty @contextproperty
def invocation_args_dict(self): def invocation_args_dict(self):
@@ -752,7 +764,7 @@ class ProviderContext(ManifestContext):
return return
elif value == arg: elif value == arg:
return return
raise DbtValidationError( raise ValidationException(
'Expected value "{}" to be one of {}'.format(value, ",".join(map(str, args))) 'Expected value "{}" to be one of {}'.format(value, ",".join(map(str, args)))
) )
@@ -767,8 +779,8 @@ class ProviderContext(ManifestContext):
@contextmember @contextmember
def write(self, payload: str) -> str: def write(self, payload: str) -> str:
# macros/source defs aren't 'writeable'. # macros/source defs aren't 'writeable'.
if isinstance(self.model, (Macro, SourceDefinition)): if isinstance(self.model, (ParsedMacro, ParsedSourceDefinition)):
raise MacrosSourcesUnWriteableError(node=self.model) raise_compiler_error('cannot "write" macros or sources')
self.model.build_path = self.model.write_node(self.config.target_path, "run", payload) self.model.build_path = self.model.write_node(self.config.target_path, "run", payload)
return "" return ""
@@ -783,19 +795,20 @@ class ProviderContext(ManifestContext):
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
except Exception: except Exception:
raise CompilationError(message_if_exception, self.model) raise_compiler_error(message_if_exception, self.model)
@contextmember @contextmember
def load_agate_table(self) -> agate.Table: def load_agate_table(self) -> agate.Table:
if not isinstance(self.model, SeedNode): if not isinstance(self.model, (ParsedSeedNode, CompiledSeedNode)):
raise LoadAgateTableNotSeedError(self.model.resource_type, node=self.model) raise_compiler_error(
assert self.model.root_path "can only load_agate_table for seeds (got a {})".format(self.model.resource_type)
)
path = os.path.join(self.model.root_path, self.model.original_file_path) path = os.path.join(self.model.root_path, self.model.original_file_path)
column_types = self.model.config.column_types column_types = self.model.config.column_types
try: try:
table = agate_helper.from_csv(path, text_columns=column_types) table = agate_helper.from_csv(path, text_columns=column_types)
except ValueError as e: except ValueError as e:
raise LoadAgateTableValueError(e, node=self.model) raise_compiler_error(str(e))
table.original_abspath = os.path.abspath(path) table.original_abspath = os.path.abspath(path)
return table return table
@@ -1186,7 +1199,7 @@ class ProviderContext(ManifestContext):
"https://docs.getdbt.com/reference/dbt-jinja-functions/dispatch)" "https://docs.getdbt.com/reference/dbt-jinja-functions/dispatch)"
" adapter_macro was called for: {macro_name}".format(macro_name=name) " adapter_macro was called for: {macro_name}".format(macro_name=name)
) )
raise CompilationError(msg) raise CompilationException(msg)
@contextmember @contextmember
def env_var(self, var: str, default: Optional[str] = None) -> str: def env_var(self, var: str, default: Optional[str] = None) -> str:
@@ -1197,7 +1210,7 @@ class ProviderContext(ManifestContext):
""" """
return_value = None return_value = None
if var.startswith(SECRET_ENV_PREFIX): if var.startswith(SECRET_ENV_PREFIX):
raise SecretEnvVarLocationError(var) disallow_secret_env_var(var)
if var in os.environ: if var in os.environ:
return_value = os.environ[var] return_value = os.environ[var]
elif default is not None: elif default is not None:
@@ -1206,13 +1219,7 @@ class ProviderContext(ManifestContext):
if return_value is not None: if return_value is not None:
# Save the env_var value in the manifest and the var name in the source_file. # Save the env_var value in the manifest and the var name in the source_file.
# If this is compiling, do not save because it's irrelevant to parsing. # If this is compiling, do not save because it's irrelevant to parsing.
compiling = ( if self.model and not hasattr(self.model, "compiled"):
True
if hasattr(self.model, "compiled")
and getattr(self.model, "compiled", False) is True
else False
)
if self.model and not compiling:
# If the environment variable is set from a default, store a string indicating # If the environment variable is set from a default, store a string indicating
# that so we can skip partial parsing. Otherwise the file will be scheduled for # that so we can skip partial parsing. Otherwise the file will be scheduled for
# reparsing. If the default changes, the file will have been updated and therefore # reparsing. If the default changes, the file will have been updated and therefore
@@ -1230,7 +1237,8 @@ class ProviderContext(ManifestContext):
source_file.env_vars.append(var) # type: ignore[union-attr] source_file.env_vars.append(var) # type: ignore[union-attr]
return return_value return return_value
else: else:
raise EnvVarMissingError(var) msg = f"Env var required but not provided: '{var}'"
raise_parsing_error(msg)
@contextproperty @contextproperty
def selected_resources(self) -> List[str]: def selected_resources(self) -> List[str]:
@@ -1249,7 +1257,7 @@ class ProviderContext(ManifestContext):
and self.context_macro_stack.call_stack[1] == "macro.dbt.statement" and self.context_macro_stack.call_stack[1] == "macro.dbt.statement"
and "materialization" in self.context_macro_stack.call_stack[0] and "materialization" in self.context_macro_stack.call_stack[0]
): ):
raise DbtRuntimeError( raise RuntimeException(
f"submit_python_job is not intended to be called here, at model {parsed_model['alias']}, with macro call_stack {self.context_macro_stack.call_stack}." f"submit_python_job is not intended to be called here, at model {parsed_model['alias']}, with macro call_stack {self.context_macro_stack.call_stack}."
) )
return self.adapter.submit_python_job(parsed_model, compiled_code) return self.adapter.submit_python_job(parsed_model, compiled_code)
@@ -1266,7 +1274,7 @@ class MacroContext(ProviderContext):
def __init__( def __init__(
self, self,
model: Macro, model: ParsedMacro,
config: RuntimeConfig, config: RuntimeConfig,
manifest: Manifest, manifest: Manifest,
provider: Provider, provider: Provider,
@@ -1381,7 +1389,7 @@ def generate_parser_model_context(
def generate_generate_name_macro_context( def generate_generate_name_macro_context(
macro: Macro, macro: ParsedMacro,
config: RuntimeConfig, config: RuntimeConfig,
manifest: Manifest, manifest: Manifest,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@@ -1399,7 +1407,7 @@ def generate_runtime_model_context(
def generate_runtime_macro_context( def generate_runtime_macro_context(
macro: Macro, macro: ParsedMacro,
config: RuntimeConfig, config: RuntimeConfig,
manifest: Manifest, manifest: Manifest,
package_name: Optional[str], package_name: Optional[str],
@@ -1411,7 +1419,7 @@ def generate_runtime_macro_context(
class ExposureRefResolver(BaseResolver): class ExposureRefResolver(BaseResolver):
def __call__(self, *args) -> str: def __call__(self, *args) -> str:
if len(args) not in (1, 2): if len(args) not in (1, 2):
raise RefArgsError(node=self.model, args=args) ref_invalid_args(self.model, args)
self.model.refs.append(list(args)) self.model.refs.append(list(args))
return "" return ""
@@ -1419,21 +1427,15 @@ class ExposureRefResolver(BaseResolver):
class ExposureSourceResolver(BaseResolver): class ExposureSourceResolver(BaseResolver):
def __call__(self, *args) -> str: def __call__(self, *args) -> str:
if len(args) != 2: if len(args) != 2:
raise NumberSourceArgsError(args, node=self.model) raise_compiler_error(
f"source() takes exactly two arguments ({len(args)} given)", self.model
)
self.model.sources.append(list(args)) self.model.sources.append(list(args))
return "" return ""
class ExposureMetricResolver(BaseResolver):
def __call__(self, *args) -> str:
if len(args) not in (1, 2):
raise MetricArgsError(node=self.model, args=args)
self.model.metrics.append(list(args))
return ""
def generate_parse_exposure( def generate_parse_exposure(
exposure: Exposure, exposure: ParsedExposure,
config: RuntimeConfig, config: RuntimeConfig,
manifest: Manifest, manifest: Manifest,
package_name: str, package_name: str,
@@ -1452,12 +1454,6 @@ def generate_parse_exposure(
project, project,
manifest, manifest,
), ),
"metric": ExposureMetricResolver(
None,
exposure,
project,
manifest,
),
} }
@@ -1469,21 +1465,21 @@ class MetricRefResolver(BaseResolver):
elif len(args) == 2: elif len(args) == 2:
package, name = args package, name = args
else: else:
raise RefArgsError(node=self.model, args=args) ref_invalid_args(self.model, args)
self.validate_args(name, package) self.validate_args(name, package)
self.model.refs.append(list(args)) self.model.refs.append(list(args))
return "" return ""
def validate_args(self, name, package): def validate_args(self, name, package):
if not isinstance(name, str): if not isinstance(name, str):
raise ParsingError( raise ParsingException(
f"In a metrics section in {self.model.original_file_path} " f"In a metrics section in {self.model.original_file_path} "
"the name argument to ref() must be a string" "the name argument to ref() must be a string"
) )
def generate_parse_metrics( def generate_parse_metrics(
metric: Metric, metric: ParsedMetric,
config: RuntimeConfig, config: RuntimeConfig,
manifest: Manifest, manifest: Manifest,
package_name: str, package_name: str,
@@ -1505,44 +1501,6 @@ def generate_parse_metrics(
} }
class EntityRefResolver(BaseResolver):
def __call__(self, *args) -> str:
package = None
if len(args) == 1:
name = args[0]
elif len(args) == 2:
package, name = args
else:
raise RefArgsError(node=self.model, args=args)
self.validate_args(name, package)
self.model.refs.append(list(args))
return ""
def validate_args(self, name, package):
if not isinstance(name, str):
raise ParsingError(
f"In the entity associated with {self.model.original_file_path} "
"the name argument to ref() must be a string"
)
def generate_parse_entities(
entity: Entity,
config: RuntimeConfig,
manifest: Manifest,
package_name: str,
) -> Dict[str, Any]:
project = config.load_dependencies()[package_name]
return {
"ref": EntityRefResolver(
None,
entity,
project,
manifest,
),
}
# This class is currently used by the schema parser in order # This class is currently used by the schema parser in order
# to limit the number of macros in the context by using # to limit the number of macros in the context by using
# the TestMacroNamespace # the TestMacroNamespace
@@ -1597,7 +1555,7 @@ class TestContext(ProviderContext):
def env_var(self, var: str, default: Optional[str] = None) -> str: def env_var(self, var: str, default: Optional[str] = None) -> str:
return_value = None return_value = None
if var.startswith(SECRET_ENV_PREFIX): if var.startswith(SECRET_ENV_PREFIX):
raise SecretEnvVarLocationError(var) disallow_secret_env_var(var)
if var in os.environ: if var in os.environ:
return_value = os.environ[var] return_value = os.environ[var]
elif default is not None: elif default is not None:
@@ -1623,7 +1581,8 @@ class TestContext(ProviderContext):
source_file.add_env_var(var, yaml_key, name) # type: ignore[union-attr] source_file.add_env_var(var, yaml_key, name) # type: ignore[union-attr]
return return_value return return_value
else: else:
raise EnvVarMissingError(var) msg = f"Env var required but not provided: '{var}'"
raise_parsing_error(msg)
def generate_test_context( def generate_test_context(

View File

@@ -4,7 +4,7 @@ from typing import Any, Dict, Optional
from .base import BaseContext, contextmember from .base import BaseContext, contextmember
from dbt.constants import SECRET_ENV_PREFIX, DEFAULT_ENV_PLACEHOLDER from dbt.constants import SECRET_ENV_PREFIX, DEFAULT_ENV_PLACEHOLDER
from dbt.exceptions import EnvVarMissingError from dbt.exceptions import raise_parsing_error
SECRET_PLACEHOLDER = "$$$DBT_SECRET_START$$${}$$$DBT_SECRET_END$$$" SECRET_PLACEHOLDER = "$$$DBT_SECRET_START$$${}$$$DBT_SECRET_END$$$"
@@ -50,7 +50,8 @@ class SecretContext(BaseContext):
self.env_vars[var] = return_value if var in os.environ else DEFAULT_ENV_PLACEHOLDER self.env_vars[var] = return_value if var in os.environ else DEFAULT_ENV_PLACEHOLDER
return return_value return return_value
else: else:
raise EnvVarMissingError(var) msg = f"Env var required but not provided: '{var}'"
raise_parsing_error(msg)
def generate_secret_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]: def generate_secret_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]:

View File

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

View File

@@ -1,16 +1,18 @@
import hashlib import hashlib
import os import os
from dataclasses import dataclass, field from dataclasses import dataclass, field
from mashumaro.types import SerializableType from mashumaro.types import SerializableType
from typing import List, Optional, Union, Dict, Any from typing import List, Optional, Union, Dict, Any
from dbt.constants import MAXIMUM_SEED_SIZE
from dbt.dataclass_schema import dbtClassMixin, StrEnum from dbt.dataclass_schema import dbtClassMixin, StrEnum
from .util import SourceKey from .util import SourceKey
MAXIMUM_SEED_SIZE = 1 * 1024 * 1024
MAXIMUM_SEED_SIZE_NAME = "1MB"
class ParseFileType(StrEnum): class ParseFileType(StrEnum):
Macro = "macro" Macro = "macro"
Model = "model" Model = "model"
@@ -227,7 +229,6 @@ class SchemaSourceFile(BaseSourceFile):
sources: List[str] = field(default_factory=list) sources: List[str] = field(default_factory=list)
exposures: List[str] = field(default_factory=list) exposures: List[str] = field(default_factory=list)
metrics: List[str] = field(default_factory=list) metrics: List[str] = field(default_factory=list)
entities: List[str] = field(default_factory=list)
# node patches contain models, seeds, snapshots, analyses # node patches contain models, seeds, snapshots, analyses
ndp: List[str] = field(default_factory=list) ndp: List[str] = field(default_factory=list)
# any macro patches in this file by macro unique_id. # any macro patches in this file by macro unique_id.

View File

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

View File

@@ -16,40 +16,44 @@ from typing import (
TypeVar, TypeVar,
Callable, Callable,
Generic, Generic,
cast,
AbstractSet, AbstractSet,
ClassVar, ClassVar,
) )
from typing_extensions import Protocol from typing_extensions import Protocol
from uuid import UUID from uuid import UUID
from dbt.contracts.graph.nodes import ( from dbt.contracts.graph.compiled import (
Macro, CompileResultNode,
Documentation,
SourceDefinition,
GenericTestNode,
Exposure,
Metric,
Entity,
UnpatchedSourceDefinition,
ManifestNode, ManifestNode,
NonSourceCompiledNode,
GraphMemberNode, GraphMemberNode,
ResultNode, )
BaseNode, from dbt.contracts.graph.parsed import (
ParsedMacro,
ParsedDocumentation,
ParsedSourceDefinition,
ParsedGenericTestNode,
ParsedExposure,
ParsedMetric,
HasUniqueID,
UnpatchedSourceDefinition,
ManifestNodes,
) )
from dbt.contracts.graph.unparsed import SourcePatch from dbt.contracts.graph.unparsed import SourcePatch
from dbt.contracts.files import SourceFile, SchemaSourceFile, FileHash, AnySourceFile from dbt.contracts.files import SourceFile, SchemaSourceFile, FileHash, AnySourceFile
from dbt.contracts.util import BaseArtifactMetadata, SourceKey, ArtifactMixin, schema_version from dbt.contracts.util import BaseArtifactMetadata, SourceKey, ArtifactMixin, schema_version
from dbt.dataclass_schema import dbtClassMixin from dbt.dataclass_schema import dbtClassMixin
from dbt.exceptions import ( from dbt.exceptions import (
CompilationError, CompilationException,
DuplicateResourceNameError, raise_duplicate_resource_name,
DuplicateMacroInPackageError, raise_compiler_error,
DuplicateMaterializationNameError,
) )
from dbt.helper_types import PathSet from dbt.helper_types import PathSet
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import MergedFromState from dbt.events.types import MergedFromState
from dbt.node_types import NodeType from dbt.node_types import NodeType
from dbt.ui import line_wrap_message
from dbt import flags from dbt import flags
from dbt import tracking from dbt import tracking
import dbt.utils import dbt.utils
@@ -92,7 +96,7 @@ class DocLookup(dbtClassMixin):
return self.perform_lookup(unique_id, manifest) return self.perform_lookup(unique_id, manifest)
return None return None
def add_doc(self, doc: Documentation): def add_doc(self, doc: ParsedDocumentation):
if doc.name not in self.storage: if doc.name not in self.storage:
self.storage[doc.name] = {} self.storage[doc.name] = {}
self.storage[doc.name][doc.package_name] = doc.unique_id self.storage[doc.name][doc.package_name] = doc.unique_id
@@ -101,9 +105,9 @@ class DocLookup(dbtClassMixin):
for doc in manifest.docs.values(): for doc in manifest.docs.values():
self.add_doc(doc) self.add_doc(doc)
def perform_lookup(self, unique_id: UniqueID, manifest) -> Documentation: def perform_lookup(self, unique_id: UniqueID, manifest) -> ParsedDocumentation:
if unique_id not in manifest.docs: if unique_id not in manifest.docs:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
f"Doc {unique_id} found in cache but not found in manifest" f"Doc {unique_id} found in cache but not found in manifest"
) )
return manifest.docs[unique_id] return manifest.docs[unique_id]
@@ -123,7 +127,7 @@ class SourceLookup(dbtClassMixin):
return self.perform_lookup(unique_id, manifest) return self.perform_lookup(unique_id, manifest)
return None return None
def add_source(self, source: SourceDefinition): def add_source(self, source: ParsedSourceDefinition):
if source.search_name not in self.storage: if source.search_name not in self.storage:
self.storage[source.search_name] = {} self.storage[source.search_name] = {}
@@ -134,9 +138,9 @@ class SourceLookup(dbtClassMixin):
if hasattr(source, "source_name"): if hasattr(source, "source_name"):
self.add_source(source) self.add_source(source)
def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> SourceDefinition: def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> ParsedSourceDefinition:
if unique_id not in manifest.sources: if unique_id not in manifest.sources:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
f"Source {unique_id} found in cache but not found in manifest" f"Source {unique_id} found in cache but not found in manifest"
) )
return manifest.sources[unique_id] return manifest.sources[unique_id]
@@ -174,7 +178,7 @@ class RefableLookup(dbtClassMixin):
def perform_lookup(self, unique_id: UniqueID, manifest) -> ManifestNode: def perform_lookup(self, unique_id: UniqueID, manifest) -> ManifestNode:
if unique_id not in manifest.nodes: if unique_id not in manifest.nodes:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
f"Node {unique_id} found in cache but not found in manifest" f"Node {unique_id} found in cache but not found in manifest"
) )
return manifest.nodes[unique_id] return manifest.nodes[unique_id]
@@ -194,7 +198,7 @@ class MetricLookup(dbtClassMixin):
return self.perform_lookup(unique_id, manifest) return self.perform_lookup(unique_id, manifest)
return None return None
def add_metric(self, metric: Metric): def add_metric(self, metric: ParsedMetric):
if metric.search_name not in self.storage: if metric.search_name not in self.storage:
self.storage[metric.search_name] = {} self.storage[metric.search_name] = {}
@@ -205,47 +209,14 @@ class MetricLookup(dbtClassMixin):
if hasattr(metric, "name"): if hasattr(metric, "name"):
self.add_metric(metric) self.add_metric(metric)
def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> Metric: def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> ParsedMetric:
if unique_id not in manifest.metrics: if unique_id not in manifest.metrics:
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
f"Metric {unique_id} found in cache but not found in manifest" f"Metric {unique_id} found in cache but not found in manifest"
) )
return manifest.metrics[unique_id] return manifest.metrics[unique_id]
class EntityLookup(dbtClassMixin):
def __init__(self, manifest: "Manifest"):
self.storage: Dict[str, Dict[PackageName, UniqueID]] = {}
self.populate(manifest)
def get_unique_id(self, search_name, package: Optional[PackageName]):
return find_unique_id_for_package(self.storage, search_name, package)
def find(self, search_name, package: Optional[PackageName], manifest: "Manifest"):
unique_id = self.get_unique_id(search_name, package)
if unique_id is not None:
return self.perform_lookup(unique_id, manifest)
return None
def add_entity(self, entity: Entity):
if entity.search_name not in self.storage:
self.storage[entity.search_name] = {}
self.storage[entity.search_name][entity.package_name] = entity.unique_id
def populate(self, manifest):
for entity in manifest.entities.values():
if hasattr(entity, "name"):
self.add_entity(entity)
def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> Entity:
if unique_id not in manifest.entities:
raise dbt.exceptions.DbtInternalError(
f"Entity {unique_id} found in cache but not found in manifest"
)
return manifest.entities[unique_id]
# This handles both models/seeds/snapshots and sources/metrics/exposures # This handles both models/seeds/snapshots and sources/metrics/exposures
class DisabledLookup(dbtClassMixin): class DisabledLookup(dbtClassMixin):
def __init__(self, manifest: "Manifest"): def __init__(self, manifest: "Manifest"):
@@ -354,7 +325,7 @@ def _sort_values(dct):
def build_node_edges(nodes: List[ManifestNode]): def build_node_edges(nodes: List[ManifestNode]):
"""Build the forward and backward edges on the given list of ManifestNodes """Build the forward and backward edges on the given list of ParsedNodes
and return them as two separate dictionaries, each mapping unique IDs to and return them as two separate dictionaries, each mapping unique IDs to
lists of edges. lists of edges.
""" """
@@ -372,10 +343,10 @@ def build_node_edges(nodes: List[ManifestNode]):
# Build a map of children of macros and generic tests # Build a map of children of macros and generic tests
def build_macro_edges(nodes: List[Any]): def build_macro_edges(nodes: List[Any]):
forward_edges: Dict[str, List[str]] = { forward_edges: Dict[str, List[str]] = {
n.unique_id: [] for n in nodes if n.unique_id.startswith("macro") or n.depends_on_macros n.unique_id: [] for n in nodes if n.unique_id.startswith("macro") or n.depends_on.macros
} }
for node in nodes: for node in nodes:
for unique_id in node.depends_on_macros: for unique_id in node.depends_on.macros:
if unique_id in forward_edges.keys(): if unique_id in forward_edges.keys():
forward_edges[unique_id].append(node.unique_id) forward_edges[unique_id].append(node.unique_id)
return _sort_values(forward_edges) return _sort_values(forward_edges)
@@ -394,7 +365,7 @@ class Locality(enum.IntEnum):
@dataclass @dataclass
class MacroCandidate: class MacroCandidate:
locality: Locality locality: Locality
macro: Macro macro: ParsedMacro
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
if not isinstance(other, MacroCandidate): if not isinstance(other, MacroCandidate):
@@ -432,7 +403,12 @@ class MaterializationCandidate(MacroCandidate):
return NotImplemented return NotImplemented
equal = self.specificity == other.specificity and self.locality == other.locality equal = self.specificity == other.specificity and self.locality == other.locality
if equal: if equal:
raise DuplicateMaterializationNameError(self.macro, other) raise_compiler_error(
"Found two materializations with the name {} (packages {} and "
"{}). dbt cannot resolve this ambiguity".format(
self.macro.name, self.macro.package_name, other.macro.package_name
)
)
return equal return equal
@@ -454,14 +430,16 @@ M = TypeVar("M", bound=MacroCandidate)
class CandidateList(List[M]): class CandidateList(List[M]):
def last(self) -> Optional[Macro]: def last(self) -> Optional[ParsedMacro]:
if not self: if not self:
return None return None
self.sort() self.sort()
return self[-1].macro return self[-1].macro
def _get_locality(macro: Macro, root_project_name: str, internal_packages: Set[str]) -> Locality: def _get_locality(
macro: ParsedMacro, root_project_name: str, internal_packages: Set[str]
) -> Locality:
if macro.package_name == root_project_name: if macro.package_name == root_project_name:
return Locality.Root return Locality.Root
elif macro.package_name in internal_packages: elif macro.package_name in internal_packages:
@@ -487,19 +465,16 @@ class Disabled(Generic[D]):
target: D target: D
MaybeMetricNode = Optional[Union[Metric, Disabled[Metric]]] MaybeMetricNode = Optional[Union[ParsedMetric, Disabled[ParsedMetric]]]
MaybeEntityNode = Optional[Union[Entity, Disabled[Entity]]] MaybeDocumentation = Optional[ParsedDocumentation]
MaybeDocumentation = Optional[Documentation]
MaybeParsedSource = Optional[ MaybeParsedSource = Optional[
Union[ Union[
SourceDefinition, ParsedSourceDefinition,
Disabled[SourceDefinition], Disabled[ParsedSourceDefinition],
] ]
] ]
@@ -517,14 +492,14 @@ def _update_into(dest: MutableMapping[str, T], new_item: T):
""" """
unique_id = new_item.unique_id unique_id = new_item.unique_id
if unique_id not in dest: if unique_id not in dest:
raise dbt.exceptions.DbtRuntimeError( raise dbt.exceptions.RuntimeException(
f"got an update_{new_item.resource_type} call with an " f"got an update_{new_item.resource_type} call with an "
f"unrecognized {new_item.resource_type}: {new_item.unique_id}" f"unrecognized {new_item.resource_type}: {new_item.unique_id}"
) )
existing = dest[unique_id] existing = dest[unique_id]
if new_item.original_file_path != existing.original_file_path: if new_item.original_file_path != existing.original_file_path:
raise dbt.exceptions.DbtRuntimeError( raise dbt.exceptions.RuntimeException(
f"cannot update a {new_item.resource_type} to have a new file path!" f"cannot update a {new_item.resource_type} to have a new file " f"path!"
) )
dest[unique_id] = new_item dest[unique_id] = new_item
@@ -539,7 +514,7 @@ class MacroMethods:
def find_macro_by_name( def find_macro_by_name(
self, name: str, root_project_name: str, package: Optional[str] self, name: str, root_project_name: str, package: Optional[str]
) -> Optional[Macro]: ) -> Optional[ParsedMacro]:
"""Find a macro in the graph by its name and package name, or None for """Find a macro in the graph by its name and package name, or None for
any package. The root project name is used to determine priority: any package. The root project name is used to determine priority:
- locally defined macros come first - locally defined macros come first
@@ -562,7 +537,7 @@ class MacroMethods:
def find_generate_macro_by_name( def find_generate_macro_by_name(
self, component: str, root_project_name: str self, component: str, root_project_name: str
) -> Optional[Macro]: ) -> Optional[ParsedMacro]:
""" """
The `generate_X_name` macros are similar to regular ones, but ignore The `generate_X_name` macros are similar to regular ones, but ignore
imported packages. imported packages.
@@ -631,12 +606,11 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
# is added it must all be added in the __reduce_ex__ method in the # is added it must all be added in the __reduce_ex__ method in the
# args tuple in the right position. # args tuple in the right position.
nodes: MutableMapping[str, ManifestNode] = field(default_factory=dict) nodes: MutableMapping[str, ManifestNode] = field(default_factory=dict)
sources: MutableMapping[str, SourceDefinition] = field(default_factory=dict) sources: MutableMapping[str, ParsedSourceDefinition] = field(default_factory=dict)
macros: MutableMapping[str, Macro] = field(default_factory=dict) macros: MutableMapping[str, ParsedMacro] = field(default_factory=dict)
docs: MutableMapping[str, Documentation] = field(default_factory=dict) docs: MutableMapping[str, ParsedDocumentation] = field(default_factory=dict)
exposures: MutableMapping[str, Exposure] = field(default_factory=dict) exposures: MutableMapping[str, ParsedExposure] = field(default_factory=dict)
metrics: MutableMapping[str, Metric] = field(default_factory=dict) metrics: MutableMapping[str, ParsedMetric] = field(default_factory=dict)
entities: MutableMapping[str, Entity] = field(default_factory=dict)
selectors: MutableMapping[str, Any] = field(default_factory=dict) selectors: MutableMapping[str, Any] = field(default_factory=dict)
files: MutableMapping[str, AnySourceFile] = field(default_factory=dict) files: MutableMapping[str, AnySourceFile] = field(default_factory=dict)
metadata: ManifestMetadata = field(default_factory=ManifestMetadata) metadata: ManifestMetadata = field(default_factory=ManifestMetadata)
@@ -658,9 +632,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
_metric_lookup: Optional[MetricLookup] = field( _metric_lookup: Optional[MetricLookup] = field(
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None} default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
) )
_entity_lookup: Optional[EntityLookup] = field(
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
)
_disabled_lookup: Optional[DisabledLookup] = field( _disabled_lookup: Optional[DisabledLookup] = field(
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None} default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
) )
@@ -687,7 +658,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
obj._lock = flags.MP_CONTEXT.Lock() obj._lock = flags.MP_CONTEXT.Lock()
return obj return obj
def sync_update_node(self, new_node: ManifestNode) -> ManifestNode: def sync_update_node(self, new_node: NonSourceCompiledNode) -> NonSourceCompiledNode:
"""update the node with a lock. The only time we should want to lock is """update the node with a lock. The only time we should want to lock is
when compiling an ephemeral ancestor of a node at runtime, because when compiling an ephemeral ancestor of a node at runtime, because
multiple threads could be just-in-time compiling the same ephemeral multiple threads could be just-in-time compiling the same ephemeral
@@ -700,24 +671,21 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
with self._lock: with self._lock:
existing = self.nodes[new_node.unique_id] existing = self.nodes[new_node.unique_id]
if getattr(existing, "compiled", False): if getattr(existing, "compiled", False):
# already compiled # already compiled -> must be a NonSourceCompiledNode
return existing return cast(NonSourceCompiledNode, existing)
_update_into(self.nodes, new_node) _update_into(self.nodes, new_node)
return new_node return new_node
def update_exposure(self, new_exposure: Exposure): def update_exposure(self, new_exposure: ParsedExposure):
_update_into(self.exposures, new_exposure) _update_into(self.exposures, new_exposure)
def update_metric(self, new_metric: Metric): def update_metric(self, new_metric: ParsedMetric):
_update_into(self.metrics, new_metric) _update_into(self.metrics, new_metric)
def update_entity(self, new_entity: Entity):
_update_into(self.entities, new_entity)
def update_node(self, new_node: ManifestNode): def update_node(self, new_node: ManifestNode):
_update_into(self.nodes, new_node) _update_into(self.nodes, new_node)
def update_source(self, new_source: SourceDefinition): def update_source(self, new_source: ParsedSourceDefinition):
_update_into(self.sources, new_source) _update_into(self.sources, new_source)
def build_flat_graph(self): def build_flat_graph(self):
@@ -729,7 +697,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self.flat_graph = { self.flat_graph = {
"exposures": {k: v.to_dict(omit_none=False) for k, v in self.exposures.items()}, "exposures": {k: v.to_dict(omit_none=False) for k, v in self.exposures.items()},
"metrics": {k: v.to_dict(omit_none=False) for k, v in self.metrics.items()}, "metrics": {k: v.to_dict(omit_none=False) for k, v in self.metrics.items()},
"entities": {k: v.to_dict(omit_none=False) for k, v in self.entities.items()},
"nodes": {k: v.to_dict(omit_none=False) for k, v in self.nodes.items()}, "nodes": {k: v.to_dict(omit_none=False) for k, v in self.nodes.items()},
"sources": {k: v.to_dict(omit_none=False) for k, v in self.sources.items()}, "sources": {k: v.to_dict(omit_none=False) for k, v in self.sources.items()},
} }
@@ -771,7 +738,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
def find_materialization_macro_by_name( def find_materialization_macro_by_name(
self, project_name: str, materialization_name: str, adapter_type: str self, project_name: str, materialization_name: str, adapter_type: str
) -> Optional[Macro]: ) -> Optional[ParsedMacro]:
candidates: CandidateList = CandidateList( candidates: CandidateList = CandidateList(
chain.from_iterable( chain.from_iterable(
self._materialization_candidates_for( self._materialization_candidates_for(
@@ -792,7 +759,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self.nodes.values(), self.nodes.values(),
self.sources.values(), self.sources.values(),
self.metrics.values(), self.metrics.values(),
self.entities.values(),
) )
for resource in all_resources: for resource in all_resources:
resource_type_plural = resource.resource_type.pluralize() resource_type_plural = resource.resource_type.pluralize()
@@ -821,7 +787,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
docs={k: _deepcopy(v) for k, v in self.docs.items()}, docs={k: _deepcopy(v) for k, v in self.docs.items()},
exposures={k: _deepcopy(v) for k, v in self.exposures.items()}, exposures={k: _deepcopy(v) for k, v in self.exposures.items()},
metrics={k: _deepcopy(v) for k, v in self.metrics.items()}, metrics={k: _deepcopy(v) for k, v in self.metrics.items()},
entities={k: _deepcopy(v) for k, v in self.entities.items()},
selectors={k: _deepcopy(v) for k, v in self.selectors.items()}, selectors={k: _deepcopy(v) for k, v in self.selectors.items()},
metadata=self.metadata, metadata=self.metadata,
disabled={k: _deepcopy(v) for k, v in self.disabled.items()}, disabled={k: _deepcopy(v) for k, v in self.disabled.items()},
@@ -838,7 +803,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self.sources.values(), self.sources.values(),
self.exposures.values(), self.exposures.values(),
self.metrics.values(), self.metrics.values(),
self.entities.values(),
) )
) )
forward_edges, backward_edges = build_node_edges(edge_members) forward_edges, backward_edges = build_node_edges(edge_members)
@@ -864,7 +828,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
docs=self.docs, docs=self.docs,
exposures=self.exposures, exposures=self.exposures,
metrics=self.metrics, metrics=self.metrics,
entities=self.entities,
selectors=self.selectors, selectors=self.selectors,
metadata=self.metadata, metadata=self.metadata,
disabled=self.disabled, disabled=self.disabled,
@@ -886,11 +849,9 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
return self.exposures[unique_id] return self.exposures[unique_id]
elif unique_id in self.metrics: elif unique_id in self.metrics:
return self.metrics[unique_id] return self.metrics[unique_id]
elif unique_id in self.entities:
return self.entities[unique_id]
else: else:
# something terrible has happened # something terrible has happened
raise dbt.exceptions.DbtInternalError( raise dbt.exceptions.InternalException(
"Expected node {} not found in manifest".format(unique_id) "Expected node {} not found in manifest".format(unique_id)
) )
@@ -924,12 +885,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self._metric_lookup = MetricLookup(self) self._metric_lookup = MetricLookup(self)
return self._metric_lookup return self._metric_lookup
@property
def entity_lookup(self) -> EntityLookup:
if self._entity_lookup is None:
self._entity_lookup = EntityLookup(self)
return self._entity_lookup
def rebuild_ref_lookup(self): def rebuild_ref_lookup(self):
self._ref_lookup = RefableLookup(self) self._ref_lookup = RefableLookup(self)
@@ -988,8 +943,8 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
search_name = f"{target_source_name}.{target_table_name}" search_name = f"{target_source_name}.{target_table_name}"
candidates = _search_packages(current_project, node_package) candidates = _search_packages(current_project, node_package)
source: Optional[SourceDefinition] = None source: Optional[ParsedSourceDefinition] = None
disabled: Optional[List[SourceDefinition]] = None disabled: Optional[List[ParsedSourceDefinition]] = None
for pkg in candidates: for pkg in candidates:
source = self.source_lookup.find(search_name, pkg, self) source = self.source_lookup.find(search_name, pkg, self)
@@ -1013,8 +968,8 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
node_package: str, node_package: str,
) -> MaybeMetricNode: ) -> MaybeMetricNode:
metric: Optional[Metric] = None metric: Optional[ParsedMetric] = None
disabled: Optional[List[Metric]] = None disabled: Optional[List[ParsedMetric]] = None
candidates = _search_packages(current_project, node_package, target_metric_package) candidates = _search_packages(current_project, node_package, target_metric_package)
for pkg in candidates: for pkg in candidates:
@@ -1030,31 +985,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
return Disabled(disabled[0]) return Disabled(disabled[0])
return None return None
def resolve_entity(
self,
target_entity_name: str,
target_entity_package: Optional[str],
current_project: str,
node_package: str,
) -> MaybeEntityNode:
entity: Optional[Entity] = None
disabled: Optional[List[Entity]] = None
candidates = _search_packages(current_project, node_package, target_entity_package)
for pkg in candidates:
entity = self.entity_lookup.find(target_entity_name, pkg, self)
if entity is not None and entity.config.enabled:
return entity
# it's possible that the node is disabled
if disabled is None:
disabled = self.disabled_lookup.find(f"{target_entity_name}", pkg)
if disabled:
return Disabled(disabled[0])
return None
# Called by DocsRuntimeContext.doc # Called by DocsRuntimeContext.doc
def resolve_doc( def resolve_doc(
self, self,
@@ -1062,7 +992,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
package: Optional[str], package: Optional[str],
current_project: str, current_project: str,
node_package: str, node_package: str,
) -> Optional[Documentation]: ) -> Optional[ParsedDocumentation]:
"""Resolve the given documentation. This follows the same algorithm as """Resolve the given documentation. This follows the same algorithm as
resolve_ref except the is_enabled checks are unnecessary as docs are resolve_ref except the is_enabled checks are unnecessary as docs are
always enabled. always enabled.
@@ -1081,7 +1011,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
adapter, adapter,
other: "WritableManifest", other: "WritableManifest",
selected: AbstractSet[UniqueID], selected: AbstractSet[UniqueID],
favor_state: bool = False,
) -> None: ) -> None:
"""Given the selected unique IDs and a writable manifest, update this """Given the selected unique IDs and a writable manifest, update this
manifest by replacing any unselected nodes with their counterpart. manifest by replacing any unselected nodes with their counterpart.
@@ -1096,10 +1025,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
node.resource_type in refables node.resource_type in refables
and not node.is_ephemeral and not node.is_ephemeral
and unique_id not in selected and unique_id not in selected
and ( and not adapter.get_relation(current.database, current.schema, current.identifier)
not adapter.get_relation(current.database, current.schema, current.identifier)
or favor_state
)
): ):
merged.add(unique_id) merged.add(unique_id)
self.nodes[unique_id] = node.replace(deferred=True) self.nodes[unique_id] = node.replace(deferred=True)
@@ -1110,14 +1036,33 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
# log up to 5 items # log up to 5 items
sample = list(islice(merged, 5)) sample = list(islice(merged, 5))
fire_event(MergedFromState(num_merged=len(merged), sample=sample)) fire_event(MergedFromState(nbr_merged=len(merged), sample=sample))
# Methods that were formerly in ParseResult # Methods that were formerly in ParseResult
def add_macro(self, source_file: SourceFile, macro: Macro): def add_macro(self, source_file: SourceFile, macro: ParsedMacro):
if macro.unique_id in self.macros: if macro.unique_id in self.macros:
# detect that the macro exists and emit an error # detect that the macro exists and emit an error
raise DuplicateMacroInPackageError(macro=macro, macro_mapping=self.macros) other_path = self.macros[macro.unique_id].original_file_path
# subtract 2 for the "Compilation Error" indent
# note that the line wrap eats newlines, so if you want newlines,
# this is the result :(
msg = line_wrap_message(
f"""\
dbt found two macros named "{macro.name}" in the project
"{macro.package_name}".
To fix this error, rename or remove one of the following
macros:
- {macro.original_file_path}
- {other_path}
""",
subtract=2,
)
raise_compiler_error(msg)
self.macros[macro.unique_id] = macro self.macros[macro.unique_id] = macro
source_file.macros.append(macro.unique_id) source_file.macros.append(macro.unique_id)
@@ -1137,39 +1082,34 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self.sources[source.unique_id] = source # type: ignore self.sources[source.unique_id] = source # type: ignore
source_file.sources.append(source.unique_id) source_file.sources.append(source.unique_id)
def add_node_nofile(self, node: ManifestNode): def add_node_nofile(self, node: ManifestNodes):
# nodes can't be overwritten! # nodes can't be overwritten!
_check_duplicates(node, self.nodes) _check_duplicates(node, self.nodes)
self.nodes[node.unique_id] = node self.nodes[node.unique_id] = node
def add_node(self, source_file: AnySourceFile, node: ManifestNode, test_from=None): def add_node(self, source_file: AnySourceFile, node: ManifestNodes, test_from=None):
self.add_node_nofile(node) self.add_node_nofile(node)
if isinstance(source_file, SchemaSourceFile): if isinstance(source_file, SchemaSourceFile):
if isinstance(node, GenericTestNode): if isinstance(node, ParsedGenericTestNode):
assert test_from assert test_from
source_file.add_test(node.unique_id, test_from) source_file.add_test(node.unique_id, test_from)
if isinstance(node, Metric): if isinstance(node, ParsedMetric):
source_file.metrics.append(node.unique_id) source_file.metrics.append(node.unique_id)
if isinstance(node, Exposure): if isinstance(node, ParsedExposure):
source_file.exposures.append(node.unique_id) source_file.exposures.append(node.unique_id)
else: else:
source_file.nodes.append(node.unique_id) source_file.nodes.append(node.unique_id)
def add_exposure(self, source_file: SchemaSourceFile, exposure: Exposure): def add_exposure(self, source_file: SchemaSourceFile, exposure: ParsedExposure):
_check_duplicates(exposure, self.exposures) _check_duplicates(exposure, self.exposures)
self.exposures[exposure.unique_id] = exposure self.exposures[exposure.unique_id] = exposure
source_file.exposures.append(exposure.unique_id) source_file.exposures.append(exposure.unique_id)
def add_metric(self, source_file: SchemaSourceFile, metric: Metric): def add_metric(self, source_file: SchemaSourceFile, metric: ParsedMetric):
_check_duplicates(metric, self.metrics) _check_duplicates(metric, self.metrics)
self.metrics[metric.unique_id] = metric self.metrics[metric.unique_id] = metric
source_file.metrics.append(metric.unique_id) source_file.metrics.append(metric.unique_id)
def add_entity(self, source_file: SchemaSourceFile, entity: Entity):
_check_duplicates(entity, self.entities)
self.entities[entity.unique_id] = entity
source_file.entities.append(entity.unique_id)
def add_disabled_nofile(self, node: GraphMemberNode): def add_disabled_nofile(self, node: GraphMemberNode):
# There can be multiple disabled nodes for the same unique_id # There can be multiple disabled nodes for the same unique_id
if node.unique_id in self.disabled: if node.unique_id in self.disabled:
@@ -1177,22 +1117,20 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
else: else:
self.disabled[node.unique_id] = [node] self.disabled[node.unique_id] = [node]
def add_disabled(self, source_file: AnySourceFile, node: ResultNode, test_from=None): def add_disabled(self, source_file: AnySourceFile, node: CompileResultNode, test_from=None):
self.add_disabled_nofile(node) self.add_disabled_nofile(node)
if isinstance(source_file, SchemaSourceFile): if isinstance(source_file, SchemaSourceFile):
if isinstance(node, GenericTestNode): if isinstance(node, ParsedGenericTestNode):
assert test_from assert test_from
source_file.add_test(node.unique_id, test_from) source_file.add_test(node.unique_id, test_from)
if isinstance(node, Metric): if isinstance(node, ParsedMetric):
source_file.metrics.append(node.unique_id) source_file.metrics.append(node.unique_id)
if isinstance(node, Entity): if isinstance(node, ParsedExposure):
source_file.entities.append(node.unique_id)
if isinstance(node, Exposure):
source_file.exposures.append(node.unique_id) source_file.exposures.append(node.unique_id)
else: else:
source_file.nodes.append(node.unique_id) source_file.nodes.append(node.unique_id)
def add_doc(self, source_file: SourceFile, doc: Documentation): def add_doc(self, source_file: SourceFile, doc: ParsedDocumentation):
_check_duplicates(doc, self.docs) _check_duplicates(doc, self.docs)
self.docs[doc.unique_id] = doc self.docs[doc.unique_id] = doc
source_file.docs.append(doc.unique_id) source_file.docs.append(doc.unique_id)
@@ -1214,7 +1152,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self.docs, self.docs,
self.exposures, self.exposures,
self.metrics, self.metrics,
self.entities,
self.selectors, self.selectors,
self.files, self.files,
self.metadata, self.metadata,
@@ -1227,7 +1164,6 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
self._source_lookup, self._source_lookup,
self._ref_lookup, self._ref_lookup,
self._metric_lookup, self._metric_lookup,
self._entity_lookup,
self._disabled_lookup, self._disabled_lookup,
self._analysis_lookup, self._analysis_lookup,
) )
@@ -1247,35 +1183,32 @@ AnyManifest = Union[Manifest, MacroManifest]
@dataclass @dataclass
@schema_version("manifest", 8) @schema_version("manifest", 7)
class WritableManifest(ArtifactMixin): class WritableManifest(ArtifactMixin):
nodes: Mapping[UniqueID, ManifestNode] = field( nodes: Mapping[UniqueID, ManifestNode] = field(
metadata=dict(description=("The nodes defined in the dbt project and its dependencies")) metadata=dict(description=("The nodes defined in the dbt project and its dependencies"))
) )
sources: Mapping[UniqueID, SourceDefinition] = field( sources: Mapping[UniqueID, ParsedSourceDefinition] = field(
metadata=dict(description=("The sources defined in the dbt project and its dependencies")) metadata=dict(description=("The sources defined in the dbt project and its dependencies"))
) )
macros: Mapping[UniqueID, Macro] = field( macros: Mapping[UniqueID, ParsedMacro] = field(
metadata=dict(description=("The macros defined in the dbt project and its dependencies")) metadata=dict(description=("The macros defined in the dbt project and its dependencies"))
) )
docs: Mapping[UniqueID, Documentation] = field( docs: Mapping[UniqueID, ParsedDocumentation] = field(
metadata=dict(description=("The docs defined in the dbt project and its dependencies")) metadata=dict(description=("The docs defined in the dbt project and its dependencies"))
) )
exposures: Mapping[UniqueID, Exposure] = field( exposures: Mapping[UniqueID, ParsedExposure] = field(
metadata=dict( metadata=dict(
description=("The exposures defined in the dbt project and its dependencies") description=("The exposures defined in the dbt project and its dependencies")
) )
) )
metrics: Mapping[UniqueID, Metric] = field( metrics: Mapping[UniqueID, ParsedMetric] = field(
metadata=dict(description=("The metrics defined in the dbt project and its dependencies")) metadata=dict(description=("The metrics defined in the dbt project and its dependencies"))
) )
entities: Mapping[UniqueID, Entity] = field(
metadata=dict(description=("The entities defined in the dbt project and its dependencies"))
)
selectors: Mapping[UniqueID, Any] = field( selectors: Mapping[UniqueID, Any] = field(
metadata=dict(description=("The selectors defined in selectors.yml")) metadata=dict(description=("The selectors defined in selectors.yml"))
) )
disabled: Optional[Mapping[UniqueID, List[ResultNode]]] = field( disabled: Optional[Mapping[UniqueID, List[CompileResultNode]]] = field(
metadata=dict(description="A mapping of the disabled nodes in the target") metadata=dict(description="A mapping of the disabled nodes in the target")
) )
parent_map: Optional[NodeEdgeMap] = field( parent_map: Optional[NodeEdgeMap] = field(
@@ -1296,8 +1229,7 @@ class WritableManifest(ArtifactMixin):
@classmethod @classmethod
def compatible_previous_versions(self): def compatible_previous_versions(self):
# return [("manifest", 4), ("manifest", 5), ("manifest", 6), ("manifest", 7)] return [("manifest", 4), ("manifest", 5), ("manifest", 6)]
return []
def __post_serialize__(self, dct): def __post_serialize__(self, dct):
for unique_id, node in dct["nodes"].items(): for unique_id, node in dct["nodes"].items():
@@ -1306,9 +1238,9 @@ class WritableManifest(ArtifactMixin):
return dct return dct
def _check_duplicates(value: BaseNode, src: Mapping[str, BaseNode]): def _check_duplicates(value: HasUniqueID, src: Mapping[str, HasUniqueID]):
if value.unique_id in src: if value.unique_id in src:
raise DuplicateResourceNameError(value, src[value.unique_id]) raise_duplicate_resource_name(value, src[value.unique_id])
K_T = TypeVar("K_T") K_T = TypeVar("K_T")
@@ -1317,7 +1249,7 @@ V_T = TypeVar("V_T")
def _expect_value(key: K_T, src: Mapping[K_T, V_T], old_file: SourceFile, name: str) -> V_T: def _expect_value(key: K_T, src: Mapping[K_T, V_T], old_file: SourceFile, name: str) -> V_T:
if key not in src: if key not in src:
raise CompilationError( raise CompilationException(
'Expected to find "{}" in cached "result.{}" based ' 'Expected to find "{}" in cached "result.{}" based '
"on cached file information: {}!".format(key, name, old_file) "on cached file information: {}!".format(key, name, old_file)
) )

View File

@@ -12,7 +12,7 @@ class MetricReference(object):
class ResolvedMetricReference(MetricReference): class ResolvedMetricReference(MetricReference):
""" """
Simple proxy over a Metric which delegates property Simple proxy over a ParsedMetric which delegates property
lookups to the underlying node. Also adds helper functions lookups to the underlying node. Also adds helper functions
for working with metrics (ie. __str__ and templating functions) for working with metrics (ie. __str__ and templating functions)
""" """

View File

@@ -9,7 +9,7 @@ from dbt.dataclass_schema import (
) )
from dbt.contracts.graph.unparsed import AdditionalPropertiesAllowed, Docs from dbt.contracts.graph.unparsed import AdditionalPropertiesAllowed, Docs
from dbt.contracts.graph.utils import validate_color from dbt.contracts.graph.utils import validate_color
from dbt.exceptions import DbtInternalError, CompilationError from dbt.exceptions import InternalException, CompilationException
from dbt.contracts.util import Replaceable, list_str from dbt.contracts.util import Replaceable, list_str
from dbt import hooks from dbt import hooks
from dbt.node_types import NodeType from dbt.node_types import NodeType
@@ -30,7 +30,7 @@ def _get_meta_value(cls: Type[M], fld: Field, key: str, default: Any) -> M:
try: try:
return cls(value) return cls(value)
except ValueError as exc: except ValueError as exc:
raise DbtInternalError(f"Invalid {cls} value: {value}") from exc raise InternalException(f"Invalid {cls} value: {value}") from exc
def _set_meta_value(obj: M, key: str, existing: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: def _set_meta_value(obj: M, key: str, existing: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
@@ -140,17 +140,17 @@ def _merge_field_value(
return _listify(self_value) + _listify(other_value) return _listify(self_value) + _listify(other_value)
elif merge_behavior == MergeBehavior.Update: elif merge_behavior == MergeBehavior.Update:
if not isinstance(self_value, dict): if not isinstance(self_value, dict):
raise DbtInternalError(f"expected dict, got {self_value}") raise InternalException(f"expected dict, got {self_value}")
if not isinstance(other_value, dict): if not isinstance(other_value, dict):
raise DbtInternalError(f"expected dict, got {other_value}") raise InternalException(f"expected dict, got {other_value}")
value = self_value.copy() value = self_value.copy()
value.update(other_value) value.update(other_value)
return value return value
elif merge_behavior == MergeBehavior.DictKeyAppend: elif merge_behavior == MergeBehavior.DictKeyAppend:
if not isinstance(self_value, dict): if not isinstance(self_value, dict):
raise DbtInternalError(f"expected dict, got {self_value}") raise InternalException(f"expected dict, got {self_value}")
if not isinstance(other_value, dict): if not isinstance(other_value, dict):
raise DbtInternalError(f"expected dict, got {other_value}") raise InternalException(f"expected dict, got {other_value}")
new_dict = {} new_dict = {}
for key in self_value.keys(): for key in self_value.keys():
new_dict[key] = _listify(self_value[key]) new_dict[key] = _listify(self_value[key])
@@ -172,7 +172,7 @@ def _merge_field_value(
return new_dict return new_dict
else: else:
raise DbtInternalError(f"Got an invalid merge_behavior: {merge_behavior}") raise InternalException(f"Got an invalid merge_behavior: {merge_behavior}")
def insensitive_patterns(*patterns: str): def insensitive_patterns(*patterns: str):
@@ -227,7 +227,7 @@ class BaseConfig(AdditionalPropertiesAllowed, Replaceable):
msg = ( msg = (
'Error, tried to delete config key "{}": Cannot delete ' "built-in keys" 'Error, tried to delete config key "{}": Cannot delete ' "built-in keys"
).format(key) ).format(key)
raise CompilationError(msg) raise CompilationException(msg)
else: else:
del self._extra[key] del self._extra[key]
@@ -368,11 +368,6 @@ class MetricConfig(BaseConfig):
enabled: bool = True enabled: bool = True
@dataclass
class EntityConfig(BaseConfig):
enabled: bool = True
@dataclass @dataclass
class ExposureConfig(BaseConfig): class ExposureConfig(BaseConfig):
enabled: bool = True enabled: bool = True
@@ -500,12 +495,6 @@ class SeedConfig(NodeConfig):
materialized: str = "seed" materialized: str = "seed"
quote_columns: Optional[bool] = None quote_columns: Optional[bool] = None
@classmethod
def validate(cls, data):
super().validate(data)
if data.get("materialized") and data.get("materialized") != "seed":
raise ValidationError("A seed must have a materialized value of 'seed'")
@dataclass @dataclass
class TestConfig(NodeAndTestConfig): class TestConfig(NodeAndTestConfig):
@@ -545,12 +534,6 @@ class TestConfig(NodeAndTestConfig):
return False return False
return True return True
@classmethod
def validate(cls, data):
super().validate(data)
if data.get("materialized") and data.get("materialized") != "test":
raise ValidationError("A test must have a materialized value of 'test'")
@dataclass @dataclass
class EmptySnapshotConfig(NodeConfig): class EmptySnapshotConfig(NodeConfig):
@@ -587,6 +570,7 @@ class SnapshotConfig(EmptySnapshotConfig):
f"Invalid value for 'check_cols': {data['check_cols']}. " f"Invalid value for 'check_cols': {data['check_cols']}. "
"Expected 'all' or a list of strings." "Expected 'all' or a list of strings."
) )
elif data.get("strategy") == "timestamp": elif data.get("strategy") == "timestamp":
if not data.get("updated_at"): if not data.get("updated_at"):
raise ValidationError( raise ValidationError(
@@ -598,9 +582,6 @@ class SnapshotConfig(EmptySnapshotConfig):
# If the strategy is not 'check' or 'timestamp' it's a custom strategy, # If the strategy is not 'check' or 'timestamp' it's a custom strategy,
# formerly supported with GenericSnapshotConfig # formerly supported with GenericSnapshotConfig
if data.get("materialized") and data.get("materialized") != "snapshot":
raise ValidationError("A snapshot must have a materialized value of 'snapshot'")
def finalize_and_validate(self): def finalize_and_validate(self):
data = self.to_dict(omit_none=True) data = self.to_dict(omit_none=True)
self.validate(data) self.validate(data)
@@ -609,7 +590,6 @@ class SnapshotConfig(EmptySnapshotConfig):
RESOURCE_TYPES: Dict[NodeType, Type[BaseConfig]] = { RESOURCE_TYPES: Dict[NodeType, Type[BaseConfig]] = {
NodeType.Metric: MetricConfig, NodeType.Metric: MetricConfig,
NodeType.Entity: EntityConfig,
NodeType.Exposure: ExposureConfig, NodeType.Exposure: ExposureConfig,
NodeType.Source: SourceConfig, NodeType.Source: SourceConfig,
NodeType.Seed: SeedConfig, NodeType.Seed: SeedConfig,

View File

@@ -11,7 +11,7 @@ from dbt.contracts.util import (
# trigger the PathEncoder # trigger the PathEncoder
import dbt.helper_types # noqa:F401 import dbt.helper_types # noqa:F401
from dbt.exceptions import CompilationError, ParsingError from dbt.exceptions import CompilationException, ParsingException
from dbt.dataclass_schema import dbtClassMixin, StrEnum, ExtensibleDbtClassMixin, ValidationError from dbt.dataclass_schema import dbtClassMixin, StrEnum, ExtensibleDbtClassMixin, ValidationError
@@ -24,6 +24,7 @@ from typing import Optional, List, Union, Dict, Any, Sequence
@dataclass @dataclass
class UnparsedBaseNode(dbtClassMixin, Replaceable): class UnparsedBaseNode(dbtClassMixin, Replaceable):
package_name: str package_name: str
root_path: str
path: str path: str
original_file_path: str original_file_path: str
@@ -87,12 +88,36 @@ class Docs(dbtClassMixin, Replaceable):
node_color: Optional[str] = None node_color: Optional[str] = None
@dataclass
class EntityRelationshipType(StrEnum):
many_to_one = "many_to_one"
one_to_many = "one_to_many"
one_to_one = "one_to_one"
def inverse(self) -> str:
if self == "many_to_one":
return "one_to_many"
elif self == "one_to_many":
return "many_to_one"
else:
return self
@dataclass
class EntityRelationship(dbtClassMixin, Replaceable):
to: str
join_key: str
relationship_type: EntityRelationshipType
@dataclass @dataclass
class HasDocs(AdditionalPropertiesMixin, ExtensibleDbtClassMixin, Replaceable): class HasDocs(AdditionalPropertiesMixin, ExtensibleDbtClassMixin, Replaceable):
name: str name: str
description: str = "" description: str = ""
meta: Dict[str, Any] = field(default_factory=dict) meta: Dict[str, Any] = field(default_factory=dict)
is_public: Optional[bool] = False
data_type: Optional[str] = None data_type: Optional[str] = None
relationships: List[EntityRelationship] = field(default_factory=list)
docs: Docs = field(default_factory=Docs) docs: Docs = field(default_factory=Docs)
_extra: Dict[str, Any] = field(default_factory=dict) _extra: Dict[str, Any] = field(default_factory=dict)
@@ -113,6 +138,9 @@ class HasTests(HasDocs):
class UnparsedColumn(HasTests): class UnparsedColumn(HasTests):
quote: Optional[bool] = None quote: Optional[bool] = None
tags: List[str] = field(default_factory=list) tags: List[str] = field(default_factory=list)
is_dimension: Optional[bool] = False
is_primary_key: Optional[bool] = False
data_type: Optional[str] = None
@dataclass @dataclass
@@ -222,7 +250,7 @@ class ExternalPartition(AdditionalPropertiesAllowed, Replaceable):
def __post_init__(self): def __post_init__(self):
if self.name == "" or self.data_type == "": if self.name == "" or self.data_type == "":
raise CompilationError("External partition columns must have names and data types") raise CompilationException("External partition columns must have names and data types")
@dataclass @dataclass
@@ -231,7 +259,7 @@ class ExternalTable(AdditionalPropertiesAllowed, Mergeable):
file_format: Optional[str] = None file_format: Optional[str] = None
row_format: Optional[str] = None row_format: Optional[str] = None
tbl_properties: Optional[str] = None tbl_properties: Optional[str] = None
partitions: Optional[Union[List[str], List[ExternalPartition]]] = None partitions: Optional[List[ExternalPartition]] = None
def __bool__(self): def __bool__(self):
return self.location is not None return self.location is not None
@@ -363,6 +391,7 @@ class SourcePatch(dbtClassMixin, Replaceable):
@dataclass @dataclass
class UnparsedDocumentation(dbtClassMixin, Replaceable): class UnparsedDocumentation(dbtClassMixin, Replaceable):
package_name: str package_name: str
root_path: str
path: str path: str
original_file_path: str original_file_path: str
@@ -484,13 +513,14 @@ class UnparsedMetric(dbtClassMixin, Replaceable):
name: str name: str
label: str label: str
calculation_method: str calculation_method: str
timestamp: str
expression: str expression: str
description: str = "" description: str = ""
timestamp: Optional[str] = None
time_grains: List[str] = field(default_factory=list) time_grains: List[str] = field(default_factory=list)
dimensions: List[str] = field(default_factory=list) dimensions: Union[Dict[str, Any], List[str]] = field(default_factory=dict)
window: Optional[MetricTime] = None window: Optional[MetricTime] = None
model: Optional[str] = None model: Optional[str] = None
allow_joins: Optional[bool] = True
filters: List[MetricFilter] = field(default_factory=list) filters: List[MetricFilter] = field(default_factory=list)
meta: Dict[str, Any] = field(default_factory=dict) meta: Dict[str, Any] = field(default_factory=dict)
tags: List[str] = field(default_factory=list) tags: List[str] = field(default_factory=list)
@@ -514,40 +544,12 @@ class UnparsedMetric(dbtClassMixin, Replaceable):
errors.append("must contain only letters, numbers and underscores") errors.append("must contain only letters, numbers and underscores")
if errors: if errors:
raise ParsingError( raise ParsingException(
f"The metric name '{data['name']}' is invalid. It {', '.join(e for e in errors)}" f"The metric name '{data['name']}' is invalid. It {', '.join(e for e in errors)}"
) )
if data.get("timestamp") is None and data.get("time_grains") is not None:
raise ValidationError(
f"The metric '{data['name']} has time_grains defined but is missing a timestamp dimension."
)
if data.get("timestamp") is None and data.get("window") is not None:
raise ValidationError(
f"The metric '{data['name']} has a window defined but is missing a timestamp dimension."
)
if data.get("model") is None and data.get("calculation_method") != "derived": if data.get("model") is None and data.get("calculation_method") != "derived":
raise ValidationError("Non-derived metrics require a 'model' property") raise ValidationError("Non-derived metrics require a 'model' property")
if data.get("model") is not None and data.get("calculation_method") == "derived": if data.get("model") is not None and data.get("calculation_method") == "derived":
raise ValidationError("Derived metrics cannot have a 'model' property") raise ValidationError("Derived metrics cannot have a 'model' property")
@dataclass
class UnparsedEntity(dbtClassMixin, Replaceable):
"""This class is used for entity information"""
name: str
model: str
description: str = ""
dimensions: List[str] = field(default_factory=list)
meta: Dict[str, Any] = field(default_factory=dict)
tags: List[str] = field(default_factory=list)
config: Dict[str, Any] = field(default_factory=dict)
@classmethod
def validate(cls, data):
super(UnparsedEntity, cls).validate(data)
# TODO: Add validation here around include/exclude and others

View File

@@ -12,7 +12,9 @@ from dataclasses import dataclass, field
from typing import Optional, List, Dict, Union, Any from typing import Optional, List, Dict, Union, Any
from mashumaro.types import SerializableType from mashumaro.types import SerializableType
PIN_PACKAGE_URL = (
"https://docs.getdbt.com/docs/package-management#section-specifying-package-versions" # noqa
)
DEFAULT_SEND_ANONYMOUS_USAGE_STATS = True DEFAULT_SEND_ANONYMOUS_USAGE_STATS = True
@@ -55,12 +57,6 @@ class LocalPackage(Package):
RawVersion = Union[str, float] RawVersion = Union[str, float]
@dataclass
class TarballPackage(Package):
tarball: str
name: str
@dataclass @dataclass
class GitPackage(Package): class GitPackage(Package):
git: str git: str
@@ -88,7 +84,7 @@ class RegistryPackage(Package):
return [str(self.version)] return [str(self.version)]
PackageSpec = Union[LocalPackage, TarballPackage, GitPackage, RegistryPackage] PackageSpec = Union[LocalPackage, GitPackage, RegistryPackage]
@dataclass @dataclass
@@ -214,7 +210,6 @@ class Project(HyphenatedDbtClassMixin, Replaceable):
sources: Dict[str, Any] = field(default_factory=dict) sources: Dict[str, Any] = field(default_factory=dict)
tests: Dict[str, Any] = field(default_factory=dict) tests: Dict[str, Any] = field(default_factory=dict)
metrics: Dict[str, Any] = field(default_factory=dict) metrics: Dict[str, Any] = field(default_factory=dict)
entities: Dict[str, Any] = field(default_factory=dict)
exposures: Dict[str, Any] = field(default_factory=dict) exposures: Dict[str, Any] = field(default_factory=dict)
vars: Optional[Dict[str, Any]] = field( vars: Optional[Dict[str, Any]] = field(
default=None, default=None,
@@ -223,7 +218,7 @@ class Project(HyphenatedDbtClassMixin, Replaceable):
), ),
) )
packages: List[PackageSpec] = field(default_factory=list) packages: List[PackageSpec] = field(default_factory=list)
query_comment: Optional[Union[QueryComment, NoValue, str]] = field(default_factory=NoValue) query_comment: Optional[Union[QueryComment, NoValue, str]] = NoValue()
@classmethod @classmethod
def validate(cls, data): def validate(cls, data):
@@ -250,7 +245,6 @@ class UserConfig(ExtensibleDbtClassMixin, Replaceable, UserConfigContract):
printer_width: Optional[int] = None printer_width: Optional[int] = None
write_json: Optional[bool] = None write_json: Optional[bool] = None
warn_error: Optional[bool] = None warn_error: Optional[bool] = None
warn_error_options: Optional[Dict[str, Union[str, List[str]]]] = None
log_format: Optional[str] = None log_format: Optional[str] = None
debug: Optional[bool] = None debug: Optional[bool] = None
version_check: Optional[bool] = None version_check: Optional[bool] = None
@@ -259,6 +253,7 @@ class UserConfig(ExtensibleDbtClassMixin, Replaceable, UserConfigContract):
static_parser: Optional[bool] = None static_parser: Optional[bool] = None
indirect_selection: Optional[str] = None indirect_selection: Optional[str] = None
cache_selected_only: Optional[bool] = None cache_selected_only: Optional[bool] = None
event_buffer_size: Optional[int] = None
@dataclass @dataclass

View File

@@ -9,7 +9,7 @@ from typing_extensions import Protocol
from dbt.dataclass_schema import dbtClassMixin, StrEnum from dbt.dataclass_schema import dbtClassMixin, StrEnum
from dbt.contracts.util import Replaceable from dbt.contracts.util import Replaceable
from dbt.exceptions import CompilationError, DataclassNotDictError from dbt.exceptions import raise_dataclass_not_dict, CompilationException
from dbt.utils import deep_merge from dbt.utils import deep_merge
@@ -43,10 +43,10 @@ class FakeAPIObject(dbtClassMixin, Replaceable, Mapping):
raise KeyError(key) from None raise KeyError(key) from None
def __iter__(self): def __iter__(self):
raise DataclassNotDictError(self) raise_dataclass_not_dict(self)
def __len__(self): def __len__(self):
raise DataclassNotDictError(self) raise_dataclass_not_dict(self)
def incorporate(self, **kwargs): def incorporate(self, **kwargs):
value = self.to_dict(omit_none=True) value = self.to_dict(omit_none=True)
@@ -88,11 +88,13 @@ class Path(FakeAPIObject):
def __post_init__(self): def __post_init__(self):
# handle pesky jinja2.Undefined sneaking in here and messing up rende # handle pesky jinja2.Undefined sneaking in here and messing up rende
if not isinstance(self.database, (type(None), str)): if not isinstance(self.database, (type(None), str)):
raise CompilationError("Got an invalid path database: {}".format(self.database)) raise CompilationException("Got an invalid path database: {}".format(self.database))
if not isinstance(self.schema, (type(None), str)): if not isinstance(self.schema, (type(None), str)):
raise CompilationError("Got an invalid path schema: {}".format(self.schema)) raise CompilationException("Got an invalid path schema: {}".format(self.schema))
if not isinstance(self.identifier, (type(None), str)): if not isinstance(self.identifier, (type(None), str)):
raise CompilationError("Got an invalid path identifier: {}".format(self.identifier)) raise CompilationException(
"Got an invalid path identifier: {}".format(self.identifier)
)
def get_lowered_part(self, key: ComponentName) -> Optional[str]: def get_lowered_part(self, key: ComponentName) -> Optional[str]:
part = self.get_part(key) part = self.get_part(key)

View File

@@ -1,5 +1,6 @@
from dbt.contracts.graph.manifest import CompileResultNode
from dbt.contracts.graph.unparsed import FreshnessThreshold from dbt.contracts.graph.unparsed import FreshnessThreshold
from dbt.contracts.graph.nodes import SourceDefinition, ResultNode from dbt.contracts.graph.parsed import ParsedSourceDefinition
from dbt.contracts.util import ( from dbt.contracts.util import (
BaseArtifactMetadata, BaseArtifactMetadata,
ArtifactMixin, ArtifactMixin,
@@ -7,13 +8,14 @@ from dbt.contracts.util import (
Replaceable, Replaceable,
schema_version, schema_version,
) )
from dbt.exceptions import DbtInternalError from dbt.exceptions import InternalException
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import TimingInfoCollected from dbt.events.types import TimingInfoCollected
from dbt.events.proto_types import RunResultMsg, TimingInfoMsg from dbt.logger import (
from dbt.events.contextvars import get_node_info TimingProcessor,
from dbt.logger import TimingProcessor JsonOnly,
from dbt.utils import lowercase, cast_to_str, cast_to_int )
from dbt.utils import lowercase
from dbt.dataclass_schema import dbtClassMixin, StrEnum from dbt.dataclass_schema import dbtClassMixin, StrEnum
import agate import agate
@@ -45,14 +47,7 @@ class TimingInfo(dbtClassMixin):
def end(self): def end(self):
self.completed_at = datetime.utcnow() self.completed_at = datetime.utcnow()
def to_msg(self):
timsg = TimingInfoMsg(
name=self.name, started_at=self.started_at, completed_at=self.completed_at
)
return timsg
# This is a context manager
class collect_timing_info: class collect_timing_info:
def __init__(self, name: str): def __init__(self, name: str):
self.timing_info = TimingInfo(name=name) self.timing_info = TimingInfo(name=name)
@@ -63,13 +58,8 @@ class collect_timing_info:
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
self.timing_info.end() self.timing_info.end()
# Note: when legacy logger is removed, we can remove the following line with JsonOnly(), TimingProcessor(self.timing_info):
with TimingProcessor(self.timing_info): fire_event(TimingInfoCollected())
fire_event(
TimingInfoCollected(
timing_info=self.timing_info.to_msg(), node_info=get_node_info()
)
)
class RunningStatus(StrEnum): class RunningStatus(StrEnum):
@@ -129,22 +119,10 @@ class BaseResult(dbtClassMixin):
data["failures"] = None data["failures"] = None
return data return data
def to_msg(self):
# TODO: add more fields
msg = RunResultMsg()
msg.status = str(self.status)
msg.message = cast_to_str(self.message)
msg.thread = self.thread_id
msg.execution_time = self.execution_time
msg.num_failures = cast_to_int(self.failures)
msg.timing_info = [ti.to_msg() for ti in self.timing]
# adapter_response
return msg
@dataclass @dataclass
class NodeResult(BaseResult): class NodeResult(BaseResult):
node: ResultNode node: CompileResultNode
@dataclass @dataclass
@@ -230,9 +208,7 @@ class RunResultsArtifact(ExecutionResult, ArtifactMixin):
generated_at: datetime, generated_at: datetime,
args: Dict, args: Dict,
): ):
processed_results = [ processed_results = [process_run_result(result) for result in results]
process_run_result(result) for result in results if isinstance(result, RunResult)
]
meta = RunResultsMetadata( meta = RunResultsMetadata(
dbt_schema_version=str(cls.dbt_schema_version), dbt_schema_version=str(cls.dbt_schema_version),
generated_at=generated_at, generated_at=generated_at,
@@ -283,7 +259,7 @@ class RunOperationResultsArtifact(RunOperationResult, ArtifactMixin):
@dataclass @dataclass
class SourceFreshnessResult(NodeResult): class SourceFreshnessResult(NodeResult):
node: SourceDefinition node: ParsedSourceDefinition
status: FreshnessStatus status: FreshnessStatus
max_loaded_at: datetime max_loaded_at: datetime
snapshotted_at: datetime snapshotted_at: datetime
@@ -343,15 +319,15 @@ def process_freshness_result(result: FreshnessNodeResult) -> FreshnessNodeOutput
# we know that this must be a SourceFreshnessResult # we know that this must be a SourceFreshnessResult
if not isinstance(result, SourceFreshnessResult): if not isinstance(result, SourceFreshnessResult):
raise DbtInternalError( raise InternalException(
"Got {} instead of a SourceFreshnessResult for a " "Got {} instead of a SourceFreshnessResult for a "
"non-error result in freshness execution!".format(type(result)) "non-error result in freshness execution!".format(type(result))
) )
# if we're here, we must have a non-None freshness threshold # if we're here, we must have a non-None freshness threshold
criteria = result.node.freshness criteria = result.node.freshness
if criteria is None: if criteria is None:
raise DbtInternalError( raise InternalException(
"Somehow evaluated a freshness result for a source that has no freshness criteria!" "Somehow evaluated a freshness result for a source " "that has no freshness criteria!"
) )
return SourceFreshnessOutput( return SourceFreshnessOutput(
unique_id=unique_id, unique_id=unique_id,

View File

@@ -5,7 +5,7 @@ from typing import Optional, List, Any, Dict, Sequence
from dbt.dataclass_schema import dbtClassMixin from dbt.dataclass_schema import dbtClassMixin
from dbt.contracts.graph.nodes import ResultNode from dbt.contracts.graph.compiled import CompileResultNode
from dbt.contracts.results import ( from dbt.contracts.results import (
RunResult, RunResult,
RunResultsArtifact, RunResultsArtifact,
@@ -32,7 +32,7 @@ class RemoteResult(VersionedSchema):
class RemoteCompileResultMixin(RemoteResult): class RemoteCompileResultMixin(RemoteResult):
raw_code: str raw_code: str
compiled_code: str compiled_code: str
node: ResultNode node: CompileResultNode
timing: List[TimingInfo] timing: List[TimingInfo]

View File

@@ -3,7 +3,7 @@ from .graph.manifest import WritableManifest
from .results import RunResultsArtifact from .results import RunResultsArtifact
from .results import FreshnessExecutionResultArtifact from .results import FreshnessExecutionResultArtifact
from typing import Optional from typing import Optional
from dbt.exceptions import IncompatibleSchemaError from dbt.exceptions import IncompatibleSchemaException
class PreviousState: class PreviousState:
@@ -19,7 +19,7 @@ class PreviousState:
if manifest_path.exists() and manifest_path.is_file(): if manifest_path.exists() and manifest_path.is_file():
try: try:
self.manifest = WritableManifest.read_and_check_versions(str(manifest_path)) self.manifest = WritableManifest.read_and_check_versions(str(manifest_path))
except IncompatibleSchemaError as exc: except IncompatibleSchemaException as exc:
exc.add_filename(str(manifest_path)) exc.add_filename(str(manifest_path))
raise raise
@@ -27,7 +27,7 @@ class PreviousState:
if results_path.exists() and results_path.is_file(): if results_path.exists() and results_path.is_file():
try: try:
self.results = RunResultsArtifact.read_and_check_versions(str(results_path)) self.results = RunResultsArtifact.read_and_check_versions(str(results_path))
except IncompatibleSchemaError as exc: except IncompatibleSchemaException as exc:
exc.add_filename(str(results_path)) exc.add_filename(str(results_path))
raise raise
@@ -37,7 +37,7 @@ class PreviousState:
self.sources = FreshnessExecutionResultArtifact.read_and_check_versions( self.sources = FreshnessExecutionResultArtifact.read_and_check_versions(
str(sources_path) str(sources_path)
) )
except IncompatibleSchemaError as exc: except IncompatibleSchemaException as exc:
exc.add_filename(str(sources_path)) exc.add_filename(str(sources_path))
raise raise
@@ -47,6 +47,6 @@ class PreviousState:
self.sources_current = FreshnessExecutionResultArtifact.read_and_check_versions( self.sources_current = FreshnessExecutionResultArtifact.read_and_check_versions(
str(sources_current_path) str(sources_current_path)
) )
except IncompatibleSchemaError as exc: except IncompatibleSchemaException as exc:
exc.add_filename(str(sources_current_path)) exc.add_filename(str(sources_current_path))
raise raise

View File

@@ -1,16 +1,17 @@
import dataclasses import dataclasses
import os
from datetime import datetime from datetime import datetime
from typing import List, Tuple, ClassVar, Type, TypeVar, Dict, Any, Optional from typing import List, Tuple, ClassVar, Type, TypeVar, Dict, Any, Optional
from dbt.clients.system import write_json, read_json from dbt.clients.system import write_json, read_json
from dbt import deprecations from dbt import deprecations
from dbt.exceptions import ( from dbt.exceptions import (
DbtInternalError, InternalException,
DbtRuntimeError, RuntimeException,
IncompatibleSchemaError, IncompatibleSchemaException,
) )
from dbt.version import __version__ from dbt.version import __version__
from dbt.events.functions import get_invocation_id, get_metadata_vars from dbt.events.functions import get_invocation_id
from dbt.dataclass_schema import dbtClassMixin from dbt.dataclass_schema import dbtClassMixin
from dbt.dataclass_schema import ( from dbt.dataclass_schema import (
@@ -123,7 +124,7 @@ class Readable:
try: try:
data = read_json(path) data = read_json(path)
except (EnvironmentError, ValueError) as exc: except (EnvironmentError, ValueError) as exc:
raise DbtRuntimeError( raise RuntimeException(
f'Could not read {cls.__name__} at "{path}" as JSON: {exc}' f'Could not read {cls.__name__} at "{path}" as JSON: {exc}'
) from exc ) from exc
@@ -147,6 +148,20 @@ class SchemaVersion:
return BASE_SCHEMAS_URL + self.path return BASE_SCHEMAS_URL + self.path
SCHEMA_VERSION_KEY = "dbt_schema_version"
METADATA_ENV_PREFIX = "DBT_ENV_CUSTOM_ENV_"
def get_metadata_env() -> Dict[str, str]:
return {
k[len(METADATA_ENV_PREFIX) :]: v
for k, v in os.environ.items()
if k.startswith(METADATA_ENV_PREFIX)
}
# This is used in the ManifestMetadata, RunResultsMetadata, RunOperationResultMetadata, # This is used in the ManifestMetadata, RunResultsMetadata, RunOperationResultMetadata,
# FreshnessMetadata, and CatalogMetadata classes # FreshnessMetadata, and CatalogMetadata classes
@dataclasses.dataclass @dataclasses.dataclass
@@ -155,7 +170,7 @@ class BaseArtifactMetadata(dbtClassMixin):
dbt_version: str = __version__ dbt_version: str = __version__
generated_at: datetime = dataclasses.field(default_factory=datetime.utcnow) generated_at: datetime = dataclasses.field(default_factory=datetime.utcnow)
invocation_id: Optional[str] = dataclasses.field(default_factory=get_invocation_id) invocation_id: Optional[str] = dataclasses.field(default_factory=get_invocation_id)
env: Dict[str, str] = dataclasses.field(default_factory=get_metadata_vars) env: Dict[str, str] = dataclasses.field(default_factory=get_metadata_env)
def __post_serialize__(self, dct): def __post_serialize__(self, dct):
dct = super().__post_serialize__(dct) dct = super().__post_serialize__(dct)
@@ -237,61 +252,16 @@ def rename_sql_attr(node_content: dict) -> dict:
return node_content return node_content
def upgrade_node_content(node_content):
rename_sql_attr(node_content)
if node_content["resource_type"] != "seed" and "root_path" in node_content:
del node_content["root_path"]
def upgrade_seed_content(node_content):
# Remove compilation related attributes
for attr_name in (
"language",
"refs",
"sources",
"metrics",
"depends_on",
"compiled_path",
"compiled",
"compiled_code",
"extra_ctes_injected",
"extra_ctes",
"relation_name",
):
if attr_name in node_content:
del node_content[attr_name]
def upgrade_manifest_json(manifest: dict) -> dict: def upgrade_manifest_json(manifest: dict) -> dict:
for node_content in manifest.get("nodes", {}).values(): for node_content in manifest.get("nodes", {}).values():
upgrade_node_content(node_content) node_content = rename_sql_attr(node_content)
if node_content["resource_type"] == "seed":
upgrade_seed_content(node_content)
for disabled in manifest.get("disabled", {}).values(): for disabled in manifest.get("disabled", {}).values():
# There can be multiple disabled nodes for the same unique_id # There can be multiple disabled nodes for the same unique_id
# so make sure all the nodes get the attr renamed # so make sure all the nodes get the attr renamed
for node_content in disabled: disabled = [rename_sql_attr(n) for n in disabled]
upgrade_node_content(node_content)
if node_content["resource_type"] == "seed":
upgrade_seed_content(node_content)
for metric_content in manifest.get("metrics", {}).values(): for metric_content in manifest.get("metrics", {}).values():
# handle attr renames + value translation ("expression" -> "derived") # handle attr renames + value translation ("expression" -> "derived")
metric_content = rename_metric_attr(metric_content) metric_content = rename_metric_attr(metric_content)
if "root_path" in metric_content:
del metric_content["root_path"]
for exposure_content in manifest.get("exposures", {}).values():
if "root_path" in exposure_content:
del exposure_content["root_path"]
for source_content in manifest.get("sources", {}).values():
if "root_path" in exposure_content:
del source_content["root_path"]
for macro_content in manifest.get("macros", {}).values():
if "root_path" in macro_content:
del macro_content["root_path"]
for doc_content in manifest.get("docs", {}).values():
if "root_path" in doc_content:
del doc_content["root_path"]
doc_content["resource_type"] = "doc"
return manifest return manifest
@@ -320,7 +290,7 @@ class VersionedSchema(dbtClassMixin):
try: try:
data = read_json(path) data = read_json(path)
except (EnvironmentError, ValueError) as exc: except (EnvironmentError, ValueError) as exc:
raise DbtRuntimeError( raise RuntimeException(
f'Could not read {cls.__name__} at "{path}" as JSON: {exc}' f'Could not read {cls.__name__} at "{path}" as JSON: {exc}'
) from exc ) from exc
@@ -332,11 +302,11 @@ class VersionedSchema(dbtClassMixin):
previous_schema_version = data["metadata"]["dbt_schema_version"] previous_schema_version = data["metadata"]["dbt_schema_version"]
# cls.dbt_schema_version is a SchemaVersion object # cls.dbt_schema_version is a SchemaVersion object
if not cls.is_compatible_version(previous_schema_version): if not cls.is_compatible_version(previous_schema_version):
raise IncompatibleSchemaError( raise IncompatibleSchemaException(
expected=str(cls.dbt_schema_version), expected=str(cls.dbt_schema_version),
found=previous_schema_version, found=previous_schema_version,
) )
if get_manifest_schema_version(data) <= 7: if get_manifest_schema_version(data) <= 6:
data = upgrade_manifest_json(data) data = upgrade_manifest_json(data)
return cls.from_dict(data) # type: ignore return cls.from_dict(data) # type: ignore
@@ -357,7 +327,7 @@ class ArtifactMixin(VersionedSchema, Writable, Readable):
def validate(cls, data): def validate(cls, data):
super().validate(data) super().validate(data)
if cls.dbt_schema_version is None: if cls.dbt_schema_version is None:
raise DbtInternalError("Cannot call from_dict with no schema version!") raise InternalException("Cannot call from_dict with no schema version!")
class Identifier(ValidatedStringMixin): class Identifier(ValidatedStringMixin):

View File

@@ -1,14 +1,14 @@
import abc
from typing import Optional, Set, List, Dict, ClassVar from typing import Optional, Set, List, Dict, ClassVar
import dbt.exceptions import dbt.exceptions
from dbt import ui
import dbt.tracking import dbt.tracking
class DBTDeprecation: class DBTDeprecation:
_name: ClassVar[Optional[str]] = None _name: ClassVar[Optional[str]] = None
_event: ClassVar[Optional[str]] = None _description: ClassVar[Optional[str]] = None
@property @property
def name(self) -> str: def name(self) -> str:
@@ -21,50 +21,66 @@ class DBTDeprecation:
dbt.tracking.track_deprecation_warn({"deprecation_name": self.name}) dbt.tracking.track_deprecation_warn({"deprecation_name": self.name})
@property @property
def event(self) -> abc.ABCMeta: def description(self) -> str:
if self._event is not None: if self._description is not None:
module_path = dbt.events.types return self._description
class_name = self._event raise NotImplementedError("description not implemented for {}".format(self))
try:
return getattr(module_path, class_name)
except AttributeError:
msg = f"Event Class `{class_name}` is not defined in `{module_path}`"
raise NameError(msg)
raise NotImplementedError("event not implemented for {}".format(self._event))
def show(self, *args, **kwargs) -> None: def show(self, *args, **kwargs) -> None:
if self.name not in active_deprecations: if self.name not in active_deprecations:
event = self.event(**kwargs) desc = self.description.format(**kwargs)
dbt.events.functions.warn_or_error(event) msg = ui.line_wrap_message(desc, prefix="Deprecated functionality\n\n")
dbt.exceptions.warn_or_error(msg, log_fmt=ui.warning_tag("{}"))
self.track_deprecation_warn() self.track_deprecation_warn()
active_deprecations.add(self.name) active_deprecations.add(self.name)
class PackageRedirectDeprecation(DBTDeprecation): class PackageRedirectDeprecation(DBTDeprecation):
_name = "package-redirect" _name = "package-redirect"
_event = "PackageRedirectDeprecation" _description = """\
The `{old_name}` package is deprecated in favor of `{new_name}`. Please update
your `packages.yml` configuration to use `{new_name}` instead.
"""
class PackageInstallPathDeprecation(DBTDeprecation): class PackageInstallPathDeprecation(DBTDeprecation):
_name = "install-packages-path" _name = "install-packages-path"
_event = "PackageInstallPathDeprecation" _description = """\
The default package install path has changed from `dbt_modules` to `dbt_packages`.
Please update `clean-targets` in `dbt_project.yml` and check `.gitignore` as well.
Or, set `packages-install-path: dbt_modules` if you'd like to keep the current value.
"""
class ConfigSourcePathDeprecation(DBTDeprecation): class ConfigPathDeprecation(DBTDeprecation):
_description = """\
The `{deprecated_path}` config has been renamed to `{exp_path}`.
Please update your `dbt_project.yml` configuration to reflect this change.
"""
class ConfigSourcePathDeprecation(ConfigPathDeprecation):
_name = "project-config-source-paths" _name = "project-config-source-paths"
_event = "ConfigSourcePathDeprecation"
class ConfigDataPathDeprecation(DBTDeprecation): class ConfigDataPathDeprecation(ConfigPathDeprecation):
_name = "project-config-data-paths" _name = "project-config-data-paths"
_event = "ConfigDataPathDeprecation"
_adapter_renamed_description = """\
The adapter function `adapter.{old_name}` is deprecated and will be removed in
a future release of dbt. Please use `adapter.{new_name}` instead.
Documentation for {new_name} can be found here:
https://docs.getdbt.com/docs/adapter
"""
def renamed_method(old_name: str, new_name: str): def renamed_method(old_name: str, new_name: str):
class AdapterDeprecationWarning(DBTDeprecation): class AdapterDeprecationWarning(DBTDeprecation):
_name = "adapter:{}".format(old_name) _name = "adapter:{}".format(old_name)
_event = "AdapterDeprecationWarning" _description = _adapter_renamed_description.format(old_name=old_name, new_name=new_name)
dep = AdapterDeprecationWarning() dep = AdapterDeprecationWarning()
deprecations_list.append(dep) deprecations_list.append(dep)
@@ -73,12 +89,26 @@ def renamed_method(old_name: str, new_name: str):
class MetricAttributesRenamed(DBTDeprecation): class MetricAttributesRenamed(DBTDeprecation):
_name = "metric-attr-renamed" _name = "metric-attr-renamed"
_event = "MetricAttributesRenamed" _description = """\
dbt-core v1.3 renamed attributes for metrics:
\n 'sql' -> 'expression'
\n 'type' -> 'calculation_method'
\n 'type: expression' -> 'calculation_method: derived'
\nThe old metric parameter names will be fully deprecated in v1.4.
\nPlease remove them from the metric definition of metric '{metric_name}'
\nRelevant issue here: https://github.com/dbt-labs/dbt-core/issues/5849
"""
class ExposureNameDeprecation(DBTDeprecation): class ExposureNameDeprecation(DBTDeprecation):
_name = "exposure-name" _name = "exposure-name"
_event = "ExposureNameDeprecation" _description = """\
Starting in v1.3, the 'name' of an exposure should contain only letters, numbers, and underscores.
Exposures support a new property, 'label', which may contain spaces, capital letters, and special characters.
{exposure} does not follow this pattern.
Please update the 'name', and use the 'label' property for a human-friendly title.
This will raise an error in a future version of dbt-core.
"""
def warn(name, *args, **kwargs): def warn(name, *args, **kwargs):
@@ -95,12 +125,12 @@ def warn(name, *args, **kwargs):
active_deprecations: Set[str] = set() active_deprecations: Set[str] = set()
deprecations_list: List[DBTDeprecation] = [ deprecations_list: List[DBTDeprecation] = [
PackageRedirectDeprecation(), ExposureNameDeprecation(),
PackageInstallPathDeprecation(),
ConfigSourcePathDeprecation(), ConfigSourcePathDeprecation(),
ConfigDataPathDeprecation(), ConfigDataPathDeprecation(),
PackageInstallPathDeprecation(),
PackageRedirectDeprecation(),
MetricAttributesRenamed(), MetricAttributesRenamed(),
ExposureNameDeprecation(),
] ]
deprecations: Dict[str, DBTDeprecation] = {d.name: d for d in deprecations_list} deprecations: Dict[str, DBTDeprecation] = {d.name: d for d in deprecations_list}

View File

@@ -16,8 +16,6 @@ Defines the base classes of `PinnedPackage` and `UnpinnedPackage`.
`downloads_directory` sets the directory packages will be downloaded to. `downloads_directory` sets the directory packages will be downloaded to.
`_install` has retry logic if the download or untarring process hit exceptions (see `dbt.utils._connection_exception_retry`).
## `git.py` ## `git.py`
Extends `PinnedPackage` and `UnpinnedPackage` specific to dbt packages defined with git urls. Extends `PinnedPackage` and `UnpinnedPackage` specific to dbt packages defined with git urls.
@@ -30,10 +28,8 @@ Extends `PinnedPackage` and `UnpinnedPackage` specific to dbt packages defined l
Extends `PinnedPackage` and `UnpinnedPackage` specific to dbt packages defined on the dbt Hub registry. Extends `PinnedPackage` and `UnpinnedPackage` specific to dbt packages defined on the dbt Hub registry.
`install` has retry logic if the download or untarring process hit exceptions (see `dbt.utils._connection_exception_retry`).
## `resolver.py` ## `resolver.py`
Resolves the package definition into package objects to download. Resolves the package definition into package objects to download.
## `tarball.py`
Extends `PinnedPackage` and `UnpinnedPackage` specific to dbt packages defined by a URL to a tarball hosted on an HTTP server.

View File

@@ -1,16 +1,13 @@
import abc import abc
import os import os
import functools
import tempfile import tempfile
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path
from typing import List, Optional, Generic, TypeVar from typing import List, Optional, Generic, TypeVar
from dbt.clients import system from dbt.clients import system
from dbt.contracts.project import ProjectPackageMetadata from dbt.contracts.project import ProjectPackageMetadata
from dbt.events.functions import fire_event from dbt.events.functions import fire_event
from dbt.events.types import DepsSetDownloadDirectory from dbt.events.types import DepsSetDownloadDirectory
from dbt.utils import _connection_exception_retry as connection_exception_retry
DOWNLOADS_PATH = None DOWNLOADS_PATH = None
@@ -77,7 +74,7 @@ class PinnedPackage(BasePackage):
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def install(self, project, renderer): def install(self, project):
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
@@ -100,34 +97,6 @@ class PinnedPackage(BasePackage):
def get_subdirectory(self): def get_subdirectory(self):
return None return None
def _install(self, project, renderer):
metadata = self.fetch_metadata(project, renderer)
tar_name = f"{self.package}.{self.version}.tar.gz"
tar_path = (Path(get_downloads_path()) / tar_name).resolve(strict=False)
system.make_directory(str(tar_path.parent))
download_url = metadata.downloads.tarball
deps_path = project.packages_install_path
package_name = self.get_project_name(project, renderer)
download_untar_fn = functools.partial(
self.download_and_untar, download_url, str(tar_path), deps_path, package_name
)
connection_exception_retry(download_untar_fn, 5)
def download_and_untar(self, download_url, tar_path, deps_path, package_name):
"""
Sometimes the download of the files fails and we want to retry. Sometimes the
download appears successful but the file did not make it through as expected
(generally due to a github incident). Either way we want to retry downloading
and untarring to see if we can get a success. Call this within
`_connection_exception_retry`
"""
system.download(download_url, tar_path)
system.untar_package(tar_path, deps_path, package_name)
SomePinned = TypeVar("SomePinned", bound=PinnedPackage) SomePinned = TypeVar("SomePinned", bound=PinnedPackage)
SomeUnpinned = TypeVar("SomeUnpinned", bound="UnpinnedPackage") SomeUnpinned = TypeVar("SomeUnpinned", bound="UnpinnedPackage")

View File

@@ -9,9 +9,14 @@ from dbt.contracts.project import (
GitPackage, GitPackage,
) )
from dbt.deps.base import PinnedPackage, UnpinnedPackage, get_downloads_path from dbt.deps.base import PinnedPackage, UnpinnedPackage, get_downloads_path
from dbt.exceptions import ExecutableError, MultipleVersionGitDepsError from dbt.exceptions import ExecutableError, warn_or_error, raise_dependency_error
from dbt.events.functions import fire_event, warn_or_error from dbt.events.functions import fire_event
from dbt.events.types import EnsureGitInstalled, DepsUnpinned from dbt.events.types import EnsureGitInstalled
from dbt import ui
PIN_PACKAGE_URL = (
"https://docs.getdbt.com/docs/package-management#section-specifying-package-versions" # noqa
)
def md5sum(s: str): def md5sum(s: str):
@@ -57,6 +62,14 @@ class GitPinnedPackage(GitPackageMixin, PinnedPackage):
else: else:
return "revision {}".format(self.revision) return "revision {}".format(self.revision)
def unpinned_msg(self):
if self.revision == "HEAD":
return "not pinned, using HEAD (default branch)"
elif self.revision in ("main", "master"):
return f'pinned to the "{self.revision}" branch'
else:
return None
def _checkout(self): def _checkout(self):
"""Performs a shallow clone of the repository into the downloads """Performs a shallow clone of the repository into the downloads
directory. This function can be called repeatedly. If the project has directory. This function can be called repeatedly. If the project has
@@ -79,8 +92,14 @@ class GitPinnedPackage(GitPackageMixin, PinnedPackage):
def _fetch_metadata(self, project, renderer) -> ProjectPackageMetadata: def _fetch_metadata(self, project, renderer) -> ProjectPackageMetadata:
path = self._checkout() path = self._checkout()
if (self.revision == "HEAD" or self.revision in ("main", "master")) and self.warn_unpinned: if self.unpinned_msg() and self.warn_unpinned:
warn_or_error(DepsUnpinned(git=self.git)) warn_or_error(
'The git package "{}" \n\tis {}.\n\tThis can introduce '
"breaking changes into your project without warning!\n\nSee {}".format(
self.git, self.unpinned_msg(), PIN_PACKAGE_URL
),
log_fmt=ui.yellow("WARNING: {}"),
)
loaded = Project.from_project_root(path, renderer) loaded = Project.from_project_root(path, renderer)
return ProjectPackageMetadata.from_project(loaded) return ProjectPackageMetadata.from_project(loaded)
@@ -143,7 +162,10 @@ class GitUnpinnedPackage(GitPackageMixin, UnpinnedPackage[GitPinnedPackage]):
if len(requested) == 0: if len(requested) == 0:
requested = {"HEAD"} requested = {"HEAD"}
elif len(requested) > 1: elif len(requested) > 1:
raise MultipleVersionGitDepsError(self.git, requested) raise_dependency_error(
"git dependencies should contain exactly one version. "
"{} contains: {}".format(self.git, requested)
)
return GitPinnedPackage( return GitPinnedPackage(
git=self.git, git=self.git,

View File

@@ -1,20 +1,23 @@
import os
import functools
from typing import List from typing import List
from dbt import semver from dbt import semver
from dbt import flags from dbt import flags
from dbt.version import get_installed_version from dbt.version import get_installed_version
from dbt.clients import registry from dbt.clients import registry, system
from dbt.contracts.project import ( from dbt.contracts.project import (
RegistryPackageMetadata, RegistryPackageMetadata,
RegistryPackage, RegistryPackage,
) )
from dbt.deps.base import PinnedPackage, UnpinnedPackage from dbt.deps.base import PinnedPackage, UnpinnedPackage, get_downloads_path
from dbt.exceptions import ( from dbt.exceptions import (
DependencyError, package_version_not_found,
PackageNotFoundError, VersionsNotCompatibleException,
PackageVersionNotFoundError, DependencyException,
VersionsNotCompatibleError, package_not_found,
) )
from dbt.utils import _connection_exception_retry as connection_exception_retry
class RegistryPackageMixin: class RegistryPackageMixin:
@@ -57,7 +60,32 @@ class RegistryPinnedPackage(RegistryPackageMixin, PinnedPackage):
return RegistryPackageMetadata.from_dict(dct) return RegistryPackageMetadata.from_dict(dct)
def install(self, project, renderer): def install(self, project, renderer):
self._install(project, renderer) metadata = self.fetch_metadata(project, renderer)
tar_name = "{}.{}.tar.gz".format(self.package, self.version)
tar_path = os.path.realpath(os.path.join(get_downloads_path(), tar_name))
system.make_directory(os.path.dirname(tar_path))
download_url = metadata.downloads.tarball
deps_path = project.packages_install_path
package_name = self.get_project_name(project, renderer)
download_untar_fn = functools.partial(
self.download_and_untar, download_url, tar_path, deps_path, package_name
)
connection_exception_retry(download_untar_fn, 5)
def download_and_untar(self, download_url, tar_path, deps_path, package_name):
"""
Sometimes the download of the files fails and we want to retry. Sometimes the
download appears successful but the file did not make it through as expected
(generally due to a github incident). Either way we want to retry downloading
and untarring to see if we can get a success. Call this within
`_connection_exception_retry`
"""
system.download(download_url, tar_path)
system.untar_package(tar_path, deps_path, package_name)
class RegistryUnpinnedPackage(RegistryPackageMixin, UnpinnedPackage[RegistryPinnedPackage]): class RegistryUnpinnedPackage(RegistryPackageMixin, UnpinnedPackage[RegistryPinnedPackage]):
@@ -71,7 +99,7 @@ class RegistryUnpinnedPackage(RegistryPackageMixin, UnpinnedPackage[RegistryPinn
def _check_in_index(self): def _check_in_index(self):
index = registry.index_cached() index = registry.index_cached()
if self.package not in index: if self.package not in index:
raise PackageNotFoundError(self.package) package_not_found(self.package)
@classmethod @classmethod
def from_contract(cls, contract: RegistryPackage) -> "RegistryUnpinnedPackage": def from_contract(cls, contract: RegistryPackage) -> "RegistryUnpinnedPackage":
@@ -95,9 +123,9 @@ class RegistryUnpinnedPackage(RegistryPackageMixin, UnpinnedPackage[RegistryPinn
self._check_in_index() self._check_in_index()
try: try:
range_ = semver.reduce_versions(*self.versions) range_ = semver.reduce_versions(*self.versions)
except VersionsNotCompatibleError as e: except VersionsNotCompatibleException as e:
new_msg = "Version error for package {}: {}".format(self.name, e) new_msg = "Version error for package {}: {}".format(self.name, e)
raise DependencyError(new_msg) from e raise DependencyException(new_msg) from e
should_version_check = bool(flags.VERSION_CHECK) should_version_check = bool(flags.VERSION_CHECK)
dbt_version = get_installed_version() dbt_version = get_installed_version()
@@ -118,9 +146,7 @@ class RegistryUnpinnedPackage(RegistryPackageMixin, UnpinnedPackage[RegistryPinn
target = None target = None
if not target: if not target:
# raise an exception if no installable target version is found # raise an exception if no installable target version is found
raise PackageVersionNotFoundError( package_version_not_found(self.package, range_, installable, should_version_check)
self.package, range_, installable, should_version_check
)
latest_compatible = installable[-1] latest_compatible = installable[-1]
return RegistryPinnedPackage( return RegistryPinnedPackage(
package=self.package, version=target, version_latest=latest_compatible package=self.package, version=target, version_latest=latest_compatible

View File

@@ -1,29 +1,22 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, List, NoReturn, Union, Type, Iterator, Set from typing import Dict, List, NoReturn, Union, Type, Iterator, Set
from dbt.exceptions import ( from dbt.exceptions import raise_dependency_error, InternalException
DuplicateDependencyToRootError,
DuplicateProjectDependencyError,
MismatchedDependencyTypeError,
DbtInternalError,
)
from dbt.config import Project, RuntimeConfig from dbt.config import Project, RuntimeConfig
from dbt.config.renderer import DbtProjectYamlRenderer from dbt.config.renderer import DbtProjectYamlRenderer
from dbt.deps.base import BasePackage, PinnedPackage, UnpinnedPackage from dbt.deps.base import BasePackage, PinnedPackage, UnpinnedPackage
from dbt.deps.local import LocalUnpinnedPackage from dbt.deps.local import LocalUnpinnedPackage
from dbt.deps.tarball import TarballUnpinnedPackage
from dbt.deps.git import GitUnpinnedPackage from dbt.deps.git import GitUnpinnedPackage
from dbt.deps.registry import RegistryUnpinnedPackage from dbt.deps.registry import RegistryUnpinnedPackage
from dbt.contracts.project import ( from dbt.contracts.project import (
LocalPackage, LocalPackage,
TarballPackage,
GitPackage, GitPackage,
RegistryPackage, RegistryPackage,
) )
PackageContract = Union[LocalPackage, TarballPackage, GitPackage, RegistryPackage] PackageContract = Union[LocalPackage, GitPackage, RegistryPackage]
@dataclass @dataclass
@@ -56,7 +49,10 @@ class PackageListing:
self.packages[key_str] = value self.packages[key_str] = value
def _mismatched_types(self, old: UnpinnedPackage, new: UnpinnedPackage) -> NoReturn: def _mismatched_types(self, old: UnpinnedPackage, new: UnpinnedPackage) -> NoReturn:
raise MismatchedDependencyTypeError(new, old) raise_dependency_error(
f"Cannot incorporate {new} ({new.__class__.__name__}) in {old} "
f"({old.__class__.__name__}): mismatched types"
)
def incorporate(self, package: UnpinnedPackage): def incorporate(self, package: UnpinnedPackage):
key: str = self._pick_key(package) key: str = self._pick_key(package)
@@ -73,14 +69,12 @@ class PackageListing:
for contract in src: for contract in src:
if isinstance(contract, LocalPackage): if isinstance(contract, LocalPackage):
pkg = LocalUnpinnedPackage.from_contract(contract) pkg = LocalUnpinnedPackage.from_contract(contract)
elif isinstance(contract, TarballPackage):
pkg = TarballUnpinnedPackage.from_contract(contract)
elif isinstance(contract, GitPackage): elif isinstance(contract, GitPackage):
pkg = GitUnpinnedPackage.from_contract(contract) pkg = GitUnpinnedPackage.from_contract(contract)
elif isinstance(contract, RegistryPackage): elif isinstance(contract, RegistryPackage):
pkg = RegistryUnpinnedPackage.from_contract(contract) pkg = RegistryUnpinnedPackage.from_contract(contract)
else: else:
raise DbtInternalError("Invalid package type {}".format(type(contract))) raise InternalException("Invalid package type {}".format(type(contract)))
self.incorporate(pkg) self.incorporate(pkg)
@classmethod @classmethod
@@ -107,9 +101,17 @@ def _check_for_duplicate_project_names(
for package in final_deps: for package in final_deps:
project_name = package.get_project_name(config, renderer) project_name = package.get_project_name(config, renderer)
if project_name in seen: if project_name in seen:
raise DuplicateProjectDependencyError(project_name) raise_dependency_error(
f'Found duplicate project "{project_name}". This occurs when '
"a dependency has the same project name as some other "
"dependency."
)
elif project_name == config.project_name: elif project_name == config.project_name:
raise DuplicateDependencyToRootError(project_name) raise_dependency_error(
"Found a dependency with the same name as the root project "
f'"{project_name}". Package names must be unique in a project.'
" Please rename one of these packages."
)
seen.add(project_name) seen.add(project_name)

View File

@@ -1,74 +0,0 @@
from dbt.contracts.project import RegistryPackageMetadata, TarballPackage
from dbt.deps.base import PinnedPackage, UnpinnedPackage
class TarballPackageMixin:
def __init__(self, tarball: str) -> None:
super().__init__()
self.tarball = tarball
@property
def name(self):
return self.tarball
def source_type(self) -> str:
return "tarball"
class TarballPinnedPackage(TarballPackageMixin, PinnedPackage):
def __init__(self, tarball: str, package: str) -> None:
super().__init__(tarball)
# setup to recycle RegistryPinnedPackage fns
self.package = package
self.version = "tarball"
@property
def name(self):
return self.package
def get_version(self):
return self.version
def nice_version_name(self):
return f"tarball (url: {self.tarball})"
def _fetch_metadata(self, project, renderer):
"""
recycle RegistryPackageMetadata so that we can use the install and
download_and_untar from RegistryPinnedPackage next.
build RegistryPackageMetadata from info passed via packages.yml since no
'metadata' service exists in this case.
"""
dct = {
"name": self.package,
"packages": [], # note: required by RegistryPackageMetadata
"downloads": {"tarball": self.tarball},
}
return RegistryPackageMetadata.from_dict(dct)
def install(self, project, renderer):
self._install(project, renderer)
class TarballUnpinnedPackage(TarballPackageMixin, UnpinnedPackage[TarballPinnedPackage]):
def __init__(
self,
tarball: str,
package: str,
) -> None:
super().__init__(tarball)
# setup to recycle RegistryPinnedPackage fns
self.package = package
self.version = "tarball"
@classmethod
def from_contract(cls, contract: TarballPackage) -> "TarballUnpinnedPackage":
return cls(tarball=contract.tarball, package=contract.name)
def incorporate(self, other: "TarballUnpinnedPackage") -> "TarballUnpinnedPackage":
return TarballUnpinnedPackage(tarball=self.tarball, package=self.package)
def resolved(self) -> TarballPinnedPackage:
return TarballPinnedPackage(tarball=self.tarball, package=self.package)

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