Compare commits

..

22 Commits

Author SHA1 Message Date
Jeremy Cohen
93572b9291 Support 'scripts' for python models 2022-09-28 20:25:53 +02:00
Gerda Shank
3562637984 Remove parsers/source.py type ignores (#5953) 2022-09-28 11:24:59 -04:00
Callum McCann
17aca39e1c Adding metric expression validation (#5873)
* adding validation

* changie

* code formatting

* updating for review

* updating tests
2022-09-27 12:38:03 -04:00
Yoshiaki Ishihara
59744f18bb Fix typos of comments in core/dbt/adapters/ (#5693) 2022-09-27 09:10:24 -07:00
Rachel
f1326f526c Runtime: Prevent introspective queries at compile (SL only) (#5926)
* Preliminary changes to keep compile from connecting to the warehouse for runtime calls

* Adds option to lib to skip connecting to warehouse for compile; adds prelim tests

* Removes unused imports

* Simplifies test and renames to SqlCompileRunnerNoIntrospection

* Updates name in tests

* Spacing

* Updates test to check for adapter connection call instead of compile and execute

* Removes commented line

* Fixes test names

* Updates plugin to postgres type as snowflake isn't available

* Fixes docstring

* Fixes formatting

* Moves conditional logic out of class

* Fixes formatting

* Removes commented line

* Moves import

* Unmoves import

* Updates changelog

* Adds further info to method docstring
2022-09-27 09:49:55 -04:00
Jeremy Cohen
834ac716fd Prefer internal macros when called explicitly (#5907)
* Add functional test

* Prefer internal macros when called explicitly

* Add changelog entry

* update tests format

Co-authored-by: Emily Rockman <emily.rockman@dbtlabs.com>
2022-09-27 08:50:55 -04:00
Doug Beatty
0487b96098 Array macros (#5823)
* Helper macro to cast from array to string

* Default implementations and tests for array macros

* Trim Trailing Whitespace

* Changelog entry

* Remove dependence upon `cast_array_to_string` macro

* pre-commit fixes

* Remove `cast_array_to_string` macro

* pre-commit fix

* Trivial direct test; array_concat/append test non-triviallly indirectly

* Remove vestigial `lstrip`
2022-09-26 13:40:15 -06:00
FishtownBuildBot
dbd36f06e4 Add new index.html and changelog yaml files from dbt-docs (#5925) 2022-09-26 14:00:04 -05:00
dave-connors-3
38ada8a68e merge exclude columns for incremental models (#5457)
* exlcude cols like in dbt_utils.star

* dispatch macro

* changelog entry

Co-authored-by: Gerda Shank <gerda@dbtlabs.com>
2022-09-26 14:49:12 -04:00
Doug Beatty
e58edaab2d Test for Koalas DataFrames (#5928) 2022-09-26 12:41:56 -06:00
Doug Beatty
c202e005cd Tests for current_timestamp (#5935)
* Tests for `current_timestamp`

* Black formatting
2022-09-26 12:31:36 -06:00
Peter Webb
8129862b3c CT-1221 add handle to changie (#5923)
* Add 'peterallenwebb' to changie's core_team list.

* fix accidental line break
2022-09-26 11:44:20 -04:00
Drew Banin
4e8aa007cf Fix adapter reset race condition in lib.py (#5921)
* (#5919) Fix adapter reset race condition in lib.py

* run black

* changie
2022-09-26 10:26:20 -04:00
Doug Beatty
fe88bfabbf Click CLI profiles directory (#5896)
* Default directories for projects and profiles

* Re-write of get_nearest_project_dir()

* Trim Trailing Whitespace

* Functionally equivalent resolvers
2022-09-24 10:47:47 -06:00
Gerda Shank
5328a64df2 CT 815 partial parsing handling of deleted metrics (#5920)
* Update delete_schema_metric to schedule referencing nodes for reparsing

* Changie
2022-09-23 18:48:00 -04:00
Chenyu Li
87c9974be1 improve error message for parsing args (#5895)
* improve error message for parsing args

* update error message

* Update models.py

* skip stack_trace for all dbt Exceptions
2022-09-23 13:31:10 -07:00
Emily Rockman
f3f509da92 update disabled metrics/exposures to use add_disabled (#5909)
* update disabled metrics/exposures to use add_disabled

* put back func fo tests

* add pp logic

* switch elif

* fix test name

* return node
2022-09-23 13:24:37 -05:00
dave-connors-3
5e8dcec2c5 update flag to -f (#5908) 2022-09-23 20:20:29 +02:00
Chenyu Li
56783446db PySpark dataframe related tests (#5906)
Co-authored-by: Doug Beatty <44704949+dbeatty10@users.noreply.github.com>
2022-09-22 16:27:27 -07:00
Chenyu Li
207cc0383d initial support for a .dbtignore (#5897) 2022-09-22 09:06:35 -07:00
leahwicz
49ecd6a6a4 Adding missing permissions on GHA (#5870)
* Adding missing permissions on GHA

* Adding read all permissions explicitly
2022-09-21 14:53:27 -04:00
dave-connors-3
c109f39d82 add optional shorthand to full refresh command (#5879)
* add optional shorthand to full refresh command

* changie

* replace full refresh with shorthand in functional test
2022-09-21 14:37:28 -04:00
83 changed files with 1537 additions and 7993 deletions

View File

@@ -0,0 +1,7 @@
kind: Docs
body: Refer to exposures by their label by default.
time: 2022-09-20T15:20:40.652948-05:00
custom:
Author: emmyoop
Issue: "306"
PR: "307"

View File

@@ -0,0 +1,7 @@
kind: Features
body: merge_exclude_columns for incremental materialization
time: 2022-07-16T14:21:16.592519-05:00
custom:
Author: dave-connors-3
Issue: "5260"
PR: "5457"

View File

@@ -0,0 +1,7 @@
kind: Features
body: Array macros
time: 2022-09-12T22:22:27.475515-06:00
custom:
Author: graciegoheen dbeatty10
Issue: "5520"
PR: "5823"

View File

@@ -0,0 +1,7 @@
kind: Features
body: add -fr flag shorthand
time: 2022-09-19T11:29:03.774678-05:00
custom:
Author: dave-connors-3
Issue: "5878"
PR: "5879"

View File

@@ -0,0 +1,7 @@
kind: Features
body: Support .dbtignore in project root to ignore certain files being read by dbt
time: 2022-09-21T14:52:22.131627-07:00
custom:
Author: ChenyuLInx
Issue: "5733"
PR: "5897"

View File

@@ -0,0 +1,9 @@
kind: Features
body: This conditionally no-ops warehouse connection at compile depending on an env
var, disabling introspection/queries during compilation only. This is a temporary
solution to more complex permissions requirements for the semantic layer.
time: 2022-09-26T13:06:27.591061-05:00
custom:
Author: racheldaniel
Issue: "5936"
PR: "5926"

View File

@@ -0,0 +1,7 @@
kind: Fixes
body: Fix typos of comments in core/dbt/adapters/
time: 2022-08-22T19:42:38.593923+09:00
custom:
Author: yoiki
Issue: "5690"
PR: "5693"

View File

@@ -0,0 +1,7 @@
kind: Fixes
body: shorthand for full refresh should be one character
time: 2022-09-22T08:39:26.948671-05:00
custom:
Author: dave-connors-3
Issue: "5878"
PR: "5908"

View File

@@ -0,0 +1,7 @@
kind: Fixes
body: Fix macro resolution order during static analysis for custom generic tests
time: 2022-09-23T14:32:26.857376+02:00
custom:
Author: jtcohen6
Issue: "5720"
PR: "5907"

View File

@@ -0,0 +1,7 @@
kind: Fixes
body: Fix race condition when invoking dbt via lib.py concurrently
time: 2022-09-23T17:45:04.405026-04:00
custom:
Author: drewbanin
Issue: "5919"
PR: "5921"

View File

@@ -0,0 +1,7 @@
kind: Under the Hood
body: Adding validation for metric expression attribute
time: 2022-09-16T15:47:12.799002-05:00
custom:
Author: callum-mcdata
Issue: "5871"
PR: "5873"

View File

@@ -0,0 +1,7 @@
kind: Under the Hood
body: Reparse references to deleted metric
time: 2022-09-23T13:35:25.681656-04:00
custom:
Author: gshank
Issue: "5444"
PR: "5920"

View File

@@ -44,7 +44,7 @@ custom:
footerFormat: |
{{- $contributorDict := dict }}
{{- /* any names added to this list should be all lowercase for later matching purposes */}}
{{- $core_team := list "emmyoop" "nathaniel-may" "gshank" "leahwicz" "chenyulinx" "stu-k" "iknox-fa" "versusfacit" "mcknight-42" "jtcohen6" "dependabot[bot]" "snyk-bot" }}
{{- $core_team := list "peterallenwebb" "emmyoop" "nathaniel-may" "gshank" "leahwicz" "chenyulinx" "stu-k" "iknox-fa" "versusfacit" "mcknight-42" "jtcohen6" "dependabot[bot]" "snyk-bot" }}
{{- range $change := .Changes }}
{{- $authorList := splitList " " $change.Custom.Author }}
{{- /* loop through all authors for a PR */}}

View File

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

View File

@@ -20,6 +20,9 @@ on:
description: 'The release version number (i.e. 1.0.0b1)'
required: true
permissions:
contents: write # this is the permission that allows creating a new release
defaults:
run:
shell: bash

View File

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

View File

@@ -3,6 +3,10 @@ on:
schedule:
- cron: "30 1 * * *"
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest

View File

@@ -20,6 +20,10 @@ on:
description: 'The version number to bump to (ex. 1.2.0, 1.3.0b1)'
required: true
permissions:
contents: write
pull-requests: write
jobs:
bump:
runs-on: ubuntu-latest

View File

@@ -384,7 +384,7 @@ class RelationsCache:
relation = self.relations.pop(old_key)
new_key = new_relation.key()
# relaton has to rename its innards, so it needs the _CachedRelation.
# relation has to rename its innards, so it needs the _CachedRelation.
relation.rename(new_relation)
# update all the relations that refer to it
for cached in self.relations.values():

View File

@@ -88,7 +88,7 @@ class AdapterProtocol( # type: ignore[misc]
],
):
# N.B. Technically these are ClassVars, but mypy doesn't support putting type vars in a
# ClassVar due to the restirctiveness of PEP-526
# ClassVar due to the restrictiveness of PEP-526
# See: https://github.com/python/mypy/issues/5144
AdapterSpecificConfigs: Type[AdapterConfig_T]
Column: Type[Column_T]

View File

@@ -2,6 +2,8 @@ from pathlib import Path, PurePath
import click
from dbt.cli.option_types import YAML
from dbt.cli.resolvers import default_project_dir, default_profiles_dir
# TODO: The name (reflected in flags) is a correction!
# The original name was `SEND_ANONYMOUS_USAGE_STATS` and used an env var called "DBT_SEND_ANONYMOUS_USAGE_STATS"
@@ -97,6 +99,7 @@ fail_fast = click.option(
full_refresh = click.option(
"--full-refresh",
"-f",
envvar="DBT_FULL_REFRESH",
help="If specified, dbt will drop incremental models and fully-recalculate the incremental table from the model definition.",
is_flag=True,
@@ -218,17 +221,15 @@ profiles_dir = click.option(
"--profiles-dir",
envvar="DBT_PROFILES_DIR",
help="Which directory to look in for the profiles.yml file. If not set, dbt will look in the current working directory first, then HOME/.dbt/",
default=PurePath.joinpath(Path.home(), ".dbt"),
type=click.Path(
exists=True,
),
default=default_profiles_dir(),
type=click.Path(exists=True),
)
project_dir = click.option(
"--project-dir",
envvar=None,
help="Which directory to look in for the dbt_project.yml file. Default is the current working directory and its parents.",
default=Path.cwd(),
default=default_project_dir(),
type=click.Path(exists=True),
)

11
core/dbt/cli/resolvers.py Normal file
View File

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

View File

@@ -12,6 +12,7 @@ import tarfile
import requests
import stat
from typing import Type, NoReturn, List, Optional, Dict, Any, Tuple, Callable, Union
from pathspec import PathSpec # type: ignore
from dbt.events.functions import fire_event
from dbt.events.types import (
@@ -36,6 +37,7 @@ def find_matching(
root_path: str,
relative_paths_to_search: List[str],
file_pattern: str,
ignore_spec: Optional[PathSpec] = None,
) -> List[Dict[str, Any]]:
"""
Given an absolute `root_path`, a list of relative paths to that
@@ -57,19 +59,30 @@ def find_matching(
reobj = re.compile(regex, re.IGNORECASE)
for relative_path_to_search in relative_paths_to_search:
# potential speedup for ignore_spec
# if ignore_spec.matches(relative_path_to_search):
# continue
absolute_path_to_search = os.path.join(root_path, relative_path_to_search)
walk_results = os.walk(absolute_path_to_search)
for current_path, subdirectories, local_files in walk_results:
# potential speedup for ignore_spec
# relative_dir = os.path.relpath(current_path, root_path) + os.sep
# if ignore_spec.match(relative_dir):
# continue
for local_file in local_files:
absolute_path = os.path.join(current_path, local_file)
relative_path = os.path.relpath(absolute_path, absolute_path_to_search)
relative_path_to_root = os.path.join(relative_path_to_search, relative_path)
modification_time = 0.0
try:
modification_time = os.path.getmtime(absolute_path)
except OSError:
fire_event(SystemErrorRetrievingModTime(path=absolute_path))
if reobj.match(local_file):
if reobj.match(local_file) and (
not ignore_spec or not ignore_spec.match_file(relative_path_to_root)
):
matching.append(
{
"searched_path": relative_path_to_search,

View File

@@ -370,11 +370,6 @@ class Compiler:
compiled_node = _compiled_type_for(node).from_dict(data)
if compiled_node.language == ModelLanguage.python:
# TODO could we also 'minify' this code at all? just aesthetic, not functional
# quoating seems like something very specific to sql so far
# for all python implementations we are seeing there's no quating.
# TODO try to find better way to do this, given that
original_quoting = self.config.quoting
self.config.quoting = {key: False for key in original_quoting.keys()}
context = self._create_node_context(compiled_node, manifest, extra_context)
@@ -385,7 +380,19 @@ class Compiler:
node,
)
# we should NOT jinja render the python model's 'raw code'
compiled_node.compiled_code = f"{node.raw_code}\n\n{postfix}"
# if the user didn't specify an explicit `model(dbt, session)` function,
# we're going to treat the user code as a "script" and wrap it in that function now.
# TODO: this is the jankiest way of doing it, with zero AST magic
if node.meta.get("missing_model_function") is True:
raw_code_lines = node.raw_code.strip().split("\n")
raw_code_lines[-1] = f"return {raw_code_lines[-1]}"
raw_code_indented = "\n ".join(raw_code_lines)
model_code = f"def model(dbt, session):\n {raw_code_indented}"
else:
model_code = node.raw_code
compiled_node.compiled_code = f"{model_code}\n\n{postfix}"
# restore quoting settings in the end since context is lazy evaluated
self.config.quoting = original_quoting

View File

@@ -156,28 +156,6 @@ def value_or(value: Optional[T], default: T) -> T:
return value
# TODO: replicate function for dbt_contracts.yml
# def _raw_contracts_from(project_root: str) -> Dict[str, Any]:
# project_root = os.path.normpath(project_root)
# project_yaml_filepath = os.path.join(project_root, "dbt_contracts.yml")
# # get the project.yml contents
# if not path_exists(project_yaml_filepath):
# raise DbtProjectError(
# "no dbt_contracts.yml found at expected path {}".format(
# project_yaml_filepath
# )
# )
# project_dict = _load_yaml(project_yaml_filepath)
# if not isinstance(project_dict, dict):
# raise DbtProjectError("dbt_contracts.yml does not parse to a dictionary")
# return project_dict
def _raw_project_from(project_root: str) -> Dict[str, Any]:
project_root = os.path.normpath(project_root)
@@ -217,8 +195,7 @@ def validate_version(dbt_version: List[VersionSpecifier], project_name: str):
installed = get_installed_version()
if not versions_compatible(*dbt_version):
msg = IMPOSSIBLE_VERSION_ERROR.format(
package=project_name,
version_spec=[x.to_version_string() for x in dbt_version],
package=project_name, version_spec=[x.to_version_string() for x in dbt_version]
)
raise DbtProjectError(msg)
@@ -374,8 +351,7 @@ class PartialProject(RenderComponents):
# `data_paths` is deprecated but still allowed. Copy it into
# `seed_paths` to simlify logic throughout the rest of the system.
seed_paths: List[str] = value_or(
cfg.seed_paths if "seed-paths" in rendered.project_dict else cfg.data_paths,
["seeds"],
cfg.seed_paths if "seed-paths" in rendered.project_dict else cfg.data_paths, ["seeds"]
)
test_paths: List[str] = value_or(cfg.test_paths, ["tests"])
analysis_paths: List[str] = value_or(cfg.analysis_paths, ["analyses"])

View File

@@ -46,7 +46,6 @@ class BaseRenderer:
def render_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
try:
# print(deep_map_render(self.render_entry, data)) # dbt_contracts.yml should output a typed dictionary
return deep_map_render(self.render_entry, data)
except RecursionException:
raise DbtProjectError(

View File

@@ -109,9 +109,15 @@ class MacroResolver:
def get_macro(self, local_package, macro_name):
local_package_macros = {}
# If the macro is explicitly prefixed with an internal namespace
# (e.g. 'dbt.some_macro'), look there first
if local_package in self.internal_package_names:
local_package_macros = self.internal_packages[local_package]
# If the macro is explicitly prefixed with a different package name
# (e.g. 'dbt_utils.some_macro'), look there first
if local_package not in self.internal_package_names and local_package in self.packages:
local_package_macros = self.packages[local_package]
# First: search the local packages for this macro
# First: search the specified package for this macro
if macro_name in local_package_macros:
return local_package_macros[macro_name]
# Now look up in the standard search order

View File

@@ -277,11 +277,13 @@ class SchemaSourceFile(BaseSourceFile):
self.tests[key][name] = []
self.tests[key][name].append(node_unique_id)
# this is only used in unit tests
def remove_tests(self, yaml_key, name):
if yaml_key in self.tests:
if name in self.tests[yaml_key]:
del self.tests[yaml_key][name]
# this is only used in tests (unit + functional)
def get_tests(self, yaml_key, name):
if yaml_key in self.tests:
if name in self.tests[yaml_key]:

View File

@@ -33,6 +33,7 @@ from dbt.contracts.graph.parsed import (
ParsedMacro,
ParsedDocumentation,
ParsedSourceDefinition,
ParsedGenericTestNode,
ParsedExposure,
ParsedMetric,
HasUniqueID,
@@ -1112,8 +1113,13 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
def add_disabled(self, source_file: AnySourceFile, node: CompileResultNode, test_from=None):
self.add_disabled_nofile(node)
if isinstance(source_file, SchemaSourceFile):
assert test_from
source_file.add_test(node.unique_id, test_from)
if isinstance(node, ParsedGenericTestNode):
assert test_from
source_file.add_test(node.unique_id, test_from)
if isinstance(node, ParsedMetric):
source_file.metrics.append(node.unique_id)
if isinstance(node, ParsedExposure):
source_file.exposures.append(node.unique_id)
else:
source_file.nodes.append(node.unique_id)

View File

@@ -622,24 +622,6 @@ class SnapshotConfig(EmptySnapshotConfig):
return self.from_dict(data)
# TODO: add a contract config to store the yaml configs in python memory
@dataclass
class ContractConfig(NodeAndTestConfig):
# this is repeated because of a different default
schema: Optional[str] = field(
default="dbt_test__audit",
metadata=CompareBehavior.Exclude.meta(),
)
materialized: str = "test"
severity: Severity = Severity("ERROR")
store_failures: Optional[bool] = None
where: Optional[str] = None
limit: Optional[int] = None
fail_calc: str = "count(*)"
warn_if: str = "!= 0"
error_if: str = "!= 0"
RESOURCE_TYPES: Dict[NodeType, Type[BaseConfig]] = {
NodeType.Metric: MetricConfig,
NodeType.Exposure: ExposureConfig,
@@ -662,5 +644,4 @@ def get_config_for(resource_type: NodeType, base=False) -> Type[BaseConfig]:
lookup = BASE_RESOURCE_TYPES
else:
lookup = RESOURCE_TYPES
# print(f"lookup config {lookup.get(resource_type, NodeConfig)}")
return lookup.get(resource_type, NodeConfig)

View File

@@ -824,8 +824,8 @@ class ParsedMetric(UnparsedBaseNode, HasUniqueID, HasFqn):
description: str
label: str
calculation_method: str
expression: str
timestamp: str
expression: str
filters: List[MetricFilter]
time_grains: List[str]
dimensions: List[str]

View File

@@ -88,14 +88,6 @@ class Docs(dbtClassMixin, Replaceable):
node_color: Optional[str] = None
@dataclass
class Contracts(dbtClassMixin, Replaceable):
# TODO: need strict typing here for various configs
# TODO: make these optional?
producer: Dict[str, Any] = field(default_factory=dict) # similar to meta
consumer: Dict[str, Any] = field(default_factory=dict)
@dataclass
class HasDocs(AdditionalPropertiesMixin, ExtensibleDbtClassMixin, Replaceable):
name: str
@@ -495,8 +487,8 @@ class UnparsedMetric(dbtClassMixin, Replaceable):
label: str
calculation_method: str
timestamp: str
expression: str
description: str = ""
expression: Union[str, int] = ""
time_grains: List[str] = field(default_factory=list)
dimensions: List[str] = field(default_factory=list)
window: Optional[MetricTime] = None

View File

@@ -352,11 +352,6 @@ class DbtProjectError(DbtConfigError):
pass
# TODO: need a config error for contracts
# class DbtContractsError(DbtConfigError):
# pass
class DbtSelectorsError(DbtConfigError):
pass

View File

@@ -50,3 +50,31 @@
{{ return(result) }}
{% endmacro %}
{% macro get_merge_update_columns(merge_update_columns, merge_exclude_columns, dest_columns) %}
{{ return(adapter.dispatch('get_merge_update_columns', 'dbt')(merge_update_columns, merge_exclude_columns, dest_columns)) }}
{% endmacro %}
{% macro default__get_merge_update_columns(merge_update_columns, merge_exclude_columns, dest_columns) %}
{%- set default_cols = dest_columns | map(attribute="quoted") | list -%}
{%- if merge_update_columns and merge_exclude_columns -%}
{{ exceptions.raise_compiler_error(
'Model cannot specify merge_update_columns and merge_exclude_columns. Please update model to use only one config'
)}}
{%- elif merge_update_columns -%}
{%- set update_columns = merge_update_columns -%}
{%- elif merge_exclude_columns -%}
{%- set update_columns = [] -%}
{%- for column in dest_columns -%}
{% if column.column | lower not in merge_exclude_columns | map("lower") | list %}
{%- do update_columns.append(column.quoted) -%}
{% endif %}
{%- endfor -%}
{%- else -%}
{%- set update_columns = default_cols -%}
{%- endif -%}
{{ return(update_columns) }}
{% endmacro %}

View File

@@ -5,7 +5,9 @@
{% macro default__get_merge_sql(target, source, unique_key, dest_columns, predicates) -%}
{%- set predicates = [] if predicates is none else [] + predicates -%}
{%- set dest_cols_csv = get_quoted_csv(dest_columns | map(attribute="name")) -%}
{%- set update_columns = config.get('merge_update_columns', default = dest_columns | map(attribute="quoted") | list) -%}
{%- set merge_update_columns = config.get('merge_update_columns') -%}
{%- set merge_exclude_columns = config.get('merge_exclude_columns') -%}
{%- set update_columns = get_merge_update_columns(merge_update_columns, merge_exclude_columns, dest_columns) -%}
{%- set sql_header = config.get('sql_header', none) -%}
{% if unique_key %}

View File

@@ -0,0 +1,8 @@
{% macro array_append(array, new_element) -%}
{{ return(adapter.dispatch('array_append', 'dbt')(array, new_element)) }}
{%- endmacro %}
{# new_element must be the same data type as elements in array to match postgres functionality #}
{% macro default__array_append(array, new_element) -%}
array_append({{ array }}, {{ new_element }})
{%- endmacro %}

View File

@@ -0,0 +1,7 @@
{% macro array_concat(array_1, array_2) -%}
{{ return(adapter.dispatch('array_concat', 'dbt')(array_1, array_2)) }}
{%- endmacro %}
{% macro default__array_concat(array_1, array_2) -%}
array_cat({{ array_1 }}, {{ array_2 }})
{%- endmacro %}

View File

@@ -0,0 +1,12 @@
{% macro array_construct(inputs=[], data_type=api.Column.translate_type('integer')) -%}
{{ return(adapter.dispatch('array_construct', 'dbt')(inputs, data_type)) }}
{%- endmacro %}
{# all inputs must be the same data type to match postgres functionality #}
{% macro default__array_construct(inputs, data_type) -%}
{% if inputs|length > 0 %}
array[ {{ inputs|join(' , ') }} ]
{% else %}
array[]::{{data_type}}[]
{% endif %}
{%- endmacro %}

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,64 @@
# TODO: this file is one big TODO
import os
from dbt.contracts.results import RunningStatus, collect_timing_info
from dbt.events.functions import fire_event
from dbt.events.types import NodeCompiling, NodeExecuting
from dbt.exceptions import RuntimeException
from dbt import flags
from collections import namedtuple
from dbt.task.sql import SqlCompileRunner
from dataclasses import dataclass
RuntimeArgs = namedtuple("RuntimeArgs", "project_dir profiles_dir single_threaded profile target")
@dataclass
class RuntimeArgs:
project_dir: str
profiles_dir: str
single_threaded: bool
profile: str
target: str
class SqlCompileRunnerNoIntrospection(SqlCompileRunner):
def compile_and_execute(self, manifest, ctx):
"""
This version of this method does not connect to the data warehouse.
As a result, introspective queries at compilation will not be supported
and will throw an error.
TODO: This is a temporary solution to more complex permissions requirements
for the semantic layer, and thus largely duplicates the code in the parent class
method. Once conditional credential usage is enabled, this should be removed.
"""
result = None
ctx.node._event_status["node_status"] = RunningStatus.Compiling
fire_event(
NodeCompiling(
node_info=ctx.node.node_info,
unique_id=ctx.node.unique_id,
)
)
with collect_timing_info("compile") as timing_info:
# if we fail here, we still have a compiled node to return
# this has the benefit of showing a build path for the errant
# model
ctx.node = self.compile(manifest)
ctx.timing.append(timing_info)
# for ephemeral nodes, we only want to compile, not run
if not ctx.node.is_ephemeral_model:
ctx.node._event_status["node_status"] = RunningStatus.Executing
fire_event(
NodeExecuting(
node_info=ctx.node.node_info,
unique_id=ctx.node.unique_id,
)
)
with collect_timing_info("execute") as timing_info:
result = self.run(ctx.node, manifest)
ctx.node = result.node
ctx.timing.append(timing_info)
return result
def get_dbt_config(project_dir, args=None, single_threaded=False):
@@ -17,27 +71,30 @@ def get_dbt_config(project_dir, args=None, single_threaded=False):
else:
profiles_dir = flags.DEFAULT_PROFILES_DIR
profile = args.profile if hasattr(args, "profile") else None
target = args.target if hasattr(args, "target") else None
# Construct a phony config
config = RuntimeConfig.from_args(
RuntimeArgs(project_dir, profiles_dir, single_threaded, profile, target)
runtime_args = RuntimeArgs(
project_dir=project_dir,
profiles_dir=profiles_dir,
single_threaded=single_threaded,
profile=getattr(args, "profile", None),
target=getattr(args, "target", None),
)
# Clear previously registered adapters--
# this fixes cacheing behavior on the dbt-server
# Construct a RuntimeConfig from phony args
config = RuntimeConfig.from_args(runtime_args)
# Set global flags from arguments
flags.set_from_args(args, config)
dbt.adapters.factory.reset_adapters()
# Load the relevant adapter
# This is idempotent, so we can call it repeatedly
dbt.adapters.factory.register_adapter(config)
# Set invocation id
# Make sure we have a valid invocation_id
dbt.events.functions.set_invocation_id()
return config
def get_task_by_type(type):
# TODO: we need to tell dbt-server what tasks are available
from dbt.task.run import RunTask
from dbt.task.list import ListTask
from dbt.task.seed import SeedTask
@@ -70,16 +127,13 @@ def create_task(type, args, manifest, config):
def no_op(*args, **kwargs):
pass
# TODO: yuck, let's rethink tasks a little
task = task(args, config)
# Wow! We can monkeypatch taskCls.load_manifest to return _our_ manifest
task.load_manifest = no_op
task.manifest = manifest
return task
def _get_operation_node(manifest, project_path, sql):
def _get_operation_node(manifest, project_path, sql, node_name):
from dbt.parser.manifest import process_node
from dbt.parser.sql import SqlBlockParser
import dbt.adapters.factory
@@ -92,26 +146,33 @@ def _get_operation_node(manifest, project_path, sql):
)
adapter = dbt.adapters.factory.get_adapter(config)
# TODO : This needs a real name?
sql_node = block_parser.parse_remote(sql, "name")
sql_node = block_parser.parse_remote(sql, node_name)
process_node(config, manifest, sql_node)
return config, sql_node, adapter
def compile_sql(manifest, project_path, sql):
from dbt.task.sql import SqlCompileRunner
def compile_sql(manifest, project_path, sql, node_name="query"):
config, node, adapter = _get_operation_node(manifest, project_path, sql, node_name)
allow_introspection = str(os.environ.get("__DBT_ALLOW_INTROSPECTION", "1")).lower() in (
"true",
"1",
"on",
)
config, node, adapter = _get_operation_node(manifest, project_path, sql)
runner = SqlCompileRunner(config, adapter, node, 1, 1)
if allow_introspection:
runner = SqlCompileRunner(config, adapter, node, 1, 1)
else:
runner = SqlCompileRunnerNoIntrospection(config, adapter, node, 1, 1)
return runner.safe_run(manifest)
def execute_sql(manifest, project_path, sql):
def execute_sql(manifest, project_path, sql, node_name="query"):
from dbt.task.sql import SqlExecuteRunner
config, node, adapter = _get_operation_node(manifest, project_path, sql)
config, node, adapter = _get_operation_node(manifest, project_path, sql, node_name)
runner = SqlExecuteRunner(config, adapter, node, 1, 1)
# TODO: use same interface for runner
return runner.safe_run(manifest)
@@ -128,5 +189,4 @@ def deserialize_manifest(manifest_msgpack):
def serialize_manifest(manifest):
# TODO: what should this take as an arg?
return manifest.to_msgpack()

View File

@@ -1,4 +1,5 @@
from typing import List
from dbt.logger import log_cache_events, log_manager
import argparse
@@ -25,7 +26,6 @@ import dbt.task.clean as clean_task
import dbt.task.compile as compile_task
import dbt.task.debug as debug_task
import dbt.task.deps as deps_task
import dbt.task.contracts as contracts_task
import dbt.task.freshness as freshness_task
import dbt.task.generate as generate_task
import dbt.task.init as init_task
@@ -45,6 +45,7 @@ import dbt.tracking
from dbt.utils import ExitCodes, args_to_dict
from dbt.config.profile import read_user_config
from dbt.exceptions import (
Exception as dbtException,
InternalException,
NotImplementedException,
FailedToConnectException,
@@ -65,11 +66,7 @@ class DBTVersion(argparse.Action):
help="show program's version number and exit",
):
super().__init__(
option_strings=option_strings,
dest=dest,
default=default,
nargs=0,
help=help,
option_strings=option_strings, dest=dest, default=default, nargs=0, help=help
)
def __call__(self, parser, namespace, values, option_string=None):
@@ -152,7 +149,8 @@ def main(args=None):
except BaseException as e:
fire_event(MainEncounteredError(exc=str(e)))
fire_event(MainStackTrace(stack_trace=traceback.format_exc()))
if not isinstance(e, dbtException):
fire_event(MainStackTrace(stack_trace=traceback.format_exc()))
exit_code = ExitCodes.UnhandledError.value
sys.exit(exit_code)
@@ -420,8 +418,6 @@ def _build_build_subparser(subparsers, base_subparser):
return sub
# TODO: Will this main.py file be completely refactored based on the latest roadmap update?
# use this function as a template for the new contract command
def _build_clean_subparser(subparsers, base_subparser):
sub = subparsers.add_parser(
"clean",
@@ -469,38 +465,6 @@ def _build_deps_subparser(subparsers, base_subparser):
return sub
def _build_contracts_subparser(subparsers, base_subparser):
sub = subparsers.add_parser(
"contracts",
parents=[base_subparser],
help="""
Pull the most recent version of the projects to consume listed in dbt_contracts.yml
""",
)
# TODO: add arguments for consumer/producer
# sub.add_argument(
# "--preview",
# action="store_true",
# help="""
# If specified, DBT will show path information for this project
# """,
# )
# _add_version_check(sub)
# sub.add_argument(
# "--publish",
# action="store_true",
# help="""
# If specified, DBT will show path information for this project
# """,
# )
# _add_version_check(sub)
sub.set_defaults(cls=contracts_task.DepsTask, which="contracts", rpc_method="contracts")
return sub
def _build_snapshot_subparser(subparsers, base_subparser):
sub = subparsers.add_parser(
"snapshot",
@@ -661,6 +625,7 @@ def _add_table_mutability_arguments(*subparsers):
for sub in subparsers:
sub.add_argument(
"--full-refresh",
"-f",
action="store_true",
help="""
If specified, dbt will drop incremental models and
@@ -928,9 +893,7 @@ def _build_run_operation_subparser(subparsers, base_subparser):
""",
)
sub.set_defaults(
cls=run_operation_task.RunOperationTask,
which="run-operation",
rpc_method="run-operation",
cls=run_operation_task.RunOperationTask, which="run-operation", rpc_method="run-operation"
)
return sub
@@ -1190,7 +1153,6 @@ def parse_args(args, cls=DBTArgumentParser):
_build_clean_subparser(subs, base_subparser)
_build_debug_subparser(subs, base_subparser)
_build_deps_subparser(subs, base_subparser)
_build_contracts_subparser(subs, base_subparser)
_build_list_subparser(subs, base_subparser)
build_sub = _build_build_subparser(subs, base_subparser)

View File

@@ -307,11 +307,6 @@ class ConfiguredParser(
else:
parsed_node.docs = Docs(show=docs_show)
# If we have contracts in the config, merge with the node level, for backwards
# compatibility with earlier node-only config.
if "contracts" in config_dict and config_dict["contracts"]:
parsed_node.meta = config_dict["contracts"]
# unrendered_config is used to compare the original database/schema/alias
# values and to handle 'same_config' and 'same_contents' calls
parsed_node.unrendered_config = config.build_config_dict(

View File

@@ -20,9 +20,7 @@ class MacroParser(BaseParser[ParsedMacro]):
# from the normal parsing flow.
def get_paths(self) -> List[FilePath]:
return filesystem_search(
project=self.project,
relative_dirs=self.project.macro_paths,
extension=".sql",
project=self.project, relative_dirs=self.project.macro_paths, extension=".sql"
)
@property

View File

@@ -60,8 +60,8 @@ class PythonValidationVisitor(ast.NodeVisitor):
)
def check_error(self, node):
if self.num_model_def != 1:
raise ParsingException("dbt only allow one model defined per python file", node=node)
if self.num_model_def > 1:
raise ParsingException("dbt only allows one model defined per python file", node=node)
if len(self.dbt_errors) != 0:
raise ParsingException("\n".join(self.dbt_errors), node=node)
@@ -86,11 +86,12 @@ class PythonParseVisitor(ast.NodeVisitor):
def _safe_eval(self, node):
try:
return ast.literal_eval(node)
except (SyntaxError, ValueError, TypeError) as exc:
msg = validator_error_message(exc)
raise ParsingException(msg, node=self.dbt_node) from exc
except (MemoryError, RecursionError) as exc:
msg = validator_error_message(exc)
except (SyntaxError, ValueError, TypeError, MemoryError, RecursionError) as exc:
msg = validator_error_message(
f"Error when trying to literal_eval an arg to dbt.ref(), dbt.source(), dbt.config() or dbt.config.get() \n{exc}\n"
"https://docs.python.org/3/library/ast.html#ast.literal_eval\n"
"In dbt python model, `dbt.ref`, `dbt.source`, `dbt.config`, `dbt.config.get` function args only support Python literal structures"
)
raise ParsingException(msg, node=self.dbt_node) from exc
def _get_call_literals(self, node):
@@ -112,7 +113,7 @@ class PythonParseVisitor(ast.NodeVisitor):
return arg_literals, kwarg_literals
def visit_Call(self, node: ast.Call) -> None:
# check weather the current call could be a dbt function call
# check whether the current call could be a dbt function call
if isinstance(node.func, ast.Attribute) and node.func.attr in dbt_function_key_words:
func_name = self._flatten_attr(node.func)
# check weather the current call really is a dbt function call
@@ -203,6 +204,13 @@ class ModelParser(SimpleSQLParser[ParsedModelNode]):
dbtValidator.visit(tree)
dbtValidator.check_error(node)
# if the user didn't specify an explicit `model(dbt, session)` function,
# we're going to treat the user code as a "script" to be wrapped in that function at compile time.
# for now, we just need to recognize that fact, and save it to the node.
if dbtValidator.num_model_def == 0:
# TODO: this is silly, put this somewhere better (outside of user space)
node.meta["missing_model_function"] = True
dbtParser = PythonParseVisitor(node)
dbtParser.visit(tree)
config_keys_used = []

View File

@@ -245,6 +245,22 @@ class PartialParsing:
if "overrides" in source:
self.remove_source_override_target(source)
def delete_disabled(self, unique_id, file_id):
# This node/metric/exposure is disabled. Find it and remove it from disabled dictionary.
for dis_index, dis_node in enumerate(self.saved_manifest.disabled[unique_id]):
if dis_node.file_id == file_id:
node = dis_node
index = dis_index
break
# Remove node from disabled
del self.saved_manifest.disabled[unique_id][index]
# if all nodes were removed for the unique id, delete the unique_id
# from the disabled dict
if not self.saved_manifest.disabled[unique_id]:
self.saved_manifest.disabled.pop(unique_id)
return node
# Deletes for all non-schema files
def delete_from_saved(self, file_id):
# Look at all things touched by file, remove those
@@ -319,15 +335,7 @@ class PartialParsing:
and unique_id in self.saved_manifest.disabled
):
# This node is disabled. Find the node and remove it from disabled dictionary.
for dis_index, dis_node in enumerate(self.saved_manifest.disabled[unique_id]):
if dis_node.file_id == source_file.file_id:
node = dis_node
break
if dis_node:
# Remove node from disabled and unique_id from disabled dict if necessary
del self.saved_manifest.disabled[unique_id][dis_index]
if not self.saved_manifest.disabled[unique_id]:
self.saved_manifest.disabled.pop(unique_id)
node = self.delete_disabled(unique_id, source_file.file_id)
else:
# Has already been deleted by another action
return
@@ -885,34 +893,40 @@ class PartialParsing:
self.add_to_pp_files(self.saved_files[macro_file_id])
# exposures are created only from schema files, so just delete
# the exposure.
# the exposure or the disabled exposure.
def delete_schema_exposure(self, schema_file, exposure_dict):
exposure_name = exposure_dict["name"]
exposures = schema_file.exposures.copy()
for unique_id in exposures:
exposure = self.saved_manifest.exposures[unique_id]
if unique_id in self.saved_manifest.exposures:
exposure = self.saved_manifest.exposures[unique_id]
if exposure.name == exposure_name:
self.deleted_manifest.exposures[unique_id] = self.saved_manifest.exposures.pop(
unique_id
)
schema_file.exposures.remove(unique_id)
fire_event(PartialParsingDeletedExposure(unique_id=unique_id))
elif unique_id in self.saved_manifest.disabled:
self.delete_disabled(unique_id, schema_file.file_id)
# metric are created only from schema files, so just delete
# the metric.
# metrics are created only from schema files, but also can be referred to by other nodes
def delete_schema_metric(self, schema_file, metric_dict):
metric_name = metric_dict["name"]
metrics = schema_file.metrics.copy()
for unique_id in metrics:
metric = self.saved_manifest.metrics[unique_id]
if unique_id in self.saved_manifest.metrics:
metric = self.saved_manifest.metrics[unique_id]
if metric.name == metric_name:
# Need to find everything that referenced this metric and schedule for parsing
if unique_id in self.saved_manifest.child_map:
self.schedule_nodes_for_parsing(self.saved_manifest.child_map[unique_id])
self.deleted_manifest.metrics[unique_id] = self.saved_manifest.metrics.pop(
unique_id
)
schema_file.metrics.remove(unique_id)
fire_event(PartialParsingDeletedMetric(id=unique_id))
elif unique_id in self.saved_manifest.disabled:
self.delete_disabled(unique_id, schema_file.file_id)
def get_schema_element(self, elem_list, elem_name):
for element in elem_list:

View File

@@ -1,3 +1,5 @@
import os
import pathspec # type: ignore
import pathlib
from dbt.clients.system import load_file_contents
from dbt.contracts.files import (
@@ -107,9 +109,9 @@ def load_seed_source_file(match: FilePath, project_name) -> SourceFile:
# Use the FilesystemSearcher to get a bunch of FilePaths, then turn
# them into a bunch of FileSource objects
def get_source_files(project, paths, extension, parse_file_type, saved_files):
def get_source_files(project, paths, extension, parse_file_type, saved_files, ignore_spec):
# file path list
fp_list = filesystem_search(project, paths, extension)
fp_list = filesystem_search(project, paths, extension, ignore_spec)
# file block list
fb_list = []
for fp in fp_list:
@@ -129,42 +131,84 @@ def get_source_files(project, paths, extension, parse_file_type, saved_files):
return fb_list
def read_files_for_parser(project, files, dirs, extensions, parse_ft, saved_files):
def read_files_for_parser(project, files, dirs, extensions, parse_ft, saved_files, ignore_spec):
parser_files = []
for extension in extensions:
source_files = get_source_files(project, dirs, extension, parse_ft, saved_files)
source_files = get_source_files(
project, dirs, extension, parse_ft, saved_files, ignore_spec
)
for sf in source_files:
files[sf.file_id] = sf
parser_files.append(sf.file_id)
return parser_files
def generate_dbt_ignore_spec(project_root):
ignore_file_path = os.path.join(project_root, ".dbtignore")
ignore_spec = None
if os.path.exists(ignore_file_path):
with open(ignore_file_path) as f:
ignore_spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, f)
return ignore_spec
# This needs to read files for multiple projects, so the 'files'
# dictionary needs to be passed in. What determines the order of
# the various projects? Is the root project always last? Do the
# non-root projects need to be done separately in order?
def read_files(project, files, parser_files, saved_files):
dbt_ignore_spec = generate_dbt_ignore_spec(project.project_root)
project_files = {}
project_files["MacroParser"] = read_files_for_parser(
project, files, project.macro_paths, [".sql"], ParseFileType.Macro, saved_files
project,
files,
project.macro_paths,
[".sql"],
ParseFileType.Macro,
saved_files,
dbt_ignore_spec,
)
project_files["ModelParser"] = read_files_for_parser(
project, files, project.model_paths, [".sql", ".py"], ParseFileType.Model, saved_files
project,
files,
project.model_paths,
[".sql", ".py"],
ParseFileType.Model,
saved_files,
dbt_ignore_spec,
)
project_files["SnapshotParser"] = read_files_for_parser(
project, files, project.snapshot_paths, [".sql"], ParseFileType.Snapshot, saved_files
project,
files,
project.snapshot_paths,
[".sql"],
ParseFileType.Snapshot,
saved_files,
dbt_ignore_spec,
)
project_files["AnalysisParser"] = read_files_for_parser(
project, files, project.analysis_paths, [".sql"], ParseFileType.Analysis, saved_files
project,
files,
project.analysis_paths,
[".sql"],
ParseFileType.Analysis,
saved_files,
dbt_ignore_spec,
)
project_files["SingularTestParser"] = read_files_for_parser(
project, files, project.test_paths, [".sql"], ParseFileType.SingularTest, saved_files
project,
files,
project.test_paths,
[".sql"],
ParseFileType.SingularTest,
saved_files,
dbt_ignore_spec,
)
# all generic tests within /tests must be nested under a /generic subfolder
@@ -175,14 +219,27 @@ def read_files(project, files, parser_files, saved_files):
[".sql"],
ParseFileType.GenericTest,
saved_files,
dbt_ignore_spec,
)
project_files["SeedParser"] = read_files_for_parser(
project, files, project.seed_paths, [".csv"], ParseFileType.Seed, saved_files
project,
files,
project.seed_paths,
[".csv"],
ParseFileType.Seed,
saved_files,
dbt_ignore_spec,
)
project_files["DocumentationParser"] = read_files_for_parser(
project, files, project.docs_paths, [".md"], ParseFileType.Documentation, saved_files
project,
files,
project.docs_paths,
[".md"],
ParseFileType.Documentation,
saved_files,
dbt_ignore_spec,
)
project_files["SchemaParser"] = read_files_for_parser(
@@ -192,6 +249,7 @@ def read_files(project, files, parser_files, saved_files):
[".yml", ".yaml"],
ParseFileType.Schema,
saved_files,
dbt_ignore_spec,
)
# Store the parser files for this particular project

View File

@@ -834,9 +834,6 @@ class NonSourceParser(YamlDocsReader, Generic[NonSourceTarget, Parsed]):
def normalize_docs_attribute(self, data, path):
return self.normalize_attribute(data, path, "docs")
def normalize_contracts_attribute(self, data, path):
return self.normalize_attribute(data, path, "contracts")
def patch_node_config(self, node, patch):
# Get the ContextConfig that's used in calculating the config
# This must match the model resource_type that's being patched
@@ -1032,7 +1029,7 @@ class ExposureParser(YamlReader):
if parsed.config.enabled:
self.manifest.add_exposure(self.yaml.file, parsed)
else:
self.manifest.add_disabled_nofile(parsed)
self.manifest.add_disabled(self.yaml.file, parsed)
def _generate_exposure_config(
self, target: UnparsedExposure, fqn: List[str], package_name: str, rendered: bool
@@ -1147,7 +1144,7 @@ class MetricParser(YamlReader):
if parsed.config.enabled:
self.manifest.add_metric(self.yaml.file, parsed)
else:
self.manifest.add_disabled_nofile(parsed)
self.manifest.add_disabled(self.yaml.file, parsed)
def _generate_metric_config(
self, target: UnparsedMetric, fqn: List[str], package_name: str, rendered: bool

View File

@@ -1,6 +1,7 @@
import os
from dataclasses import dataclass
from typing import List, Callable, Iterable, Set, Union, Iterator, TypeVar, Generic
from typing import List, Callable, Iterable, Set, Union, Iterator, TypeVar, Generic, Optional
from pathspec import PathSpec # type: ignore
from dbt.clients.jinja import extract_toplevel_blocks, BlockTag
from dbt.clients.system import find_matching
@@ -61,11 +62,16 @@ class FullBlock(FileBlock):
return self.block.full_block
def filesystem_search(project: Project, relative_dirs: List[str], extension: str):
def filesystem_search(
project: Project,
relative_dirs: List[str],
extension: str,
ignore_spec: Optional[PathSpec] = None,
):
ext = "[!.#~]*" + extension
root = project.project_root
file_path_list = []
for result in find_matching(root, relative_dirs, ext):
for result in find_matching(root, relative_dirs, ext, ignore_spec):
if "searched_path" not in result or "relative_path" not in result:
raise InternalException("Invalid result from find_matching: {}".format(result))
file_match = FilePath(

View File

@@ -63,7 +63,7 @@ class SourcePatcher:
self.sources[unpatched.unique_id] = unpatched
continue
# returns None if there is no patch
patch = self.get_patch_for(unpatched) # type: ignore[unreachable] # CT-564 / GH 5169
patch = self.get_patch_for(unpatched)
# returns unpatched if there is no patch
patched = self.patch_source(unpatched, patch)
@@ -213,8 +213,8 @@ class SourcePatcher:
self,
unpatched: UnpatchedSourceDefinition,
) -> Optional[SourcePatch]:
if isinstance(unpatched, ParsedSourceDefinition): # type: ignore[unreachable] # CT-564 / GH 5169
return None # type: ignore[unreachable] # CT-564 / GH 5169
if isinstance(unpatched, ParsedSourceDefinition):
return None
key = (unpatched.package_name, unpatched.source.name)
patch: Optional[SourcePatch] = self.manifest.source_patches.get(key)
if patch is None:

View File

@@ -1,179 +0,0 @@
# coding=utf-8
# # TODO: use dbt deps and debug code as a template to print out helpful information for the dbt conrtracts command
import os
import shutil
import json
import dbt.utils
import dbt.deprecations
import dbt.exceptions
from dbt.ui import green, red
from dbt.config import UnsetProfileConfig
# from dbt.config.renderer import DbtProjectYamlRenderer
# from dbt.deps.base import downloads_directory
# from dbt.deps.resolver import resolve_packages
# from dbt.events.functions import fire_event
# from dbt.events.types import (
# DepsNoPackagesFound,
# DepsStartPackageInstall,
# DepsUpdateAvailable,
# DepsUTD,
# DepsInstallInfo,
# DepsListSubdirectory,
# DepsNotifyUpdatesAvailable,
# EmptyLine,
# )
# from dbt.clients import system
from dbt.task.base import BaseTask, move_to_nearest_project_dir
from dbt.clients.yaml_helper import load_yaml_text
# from dbt.clients.git import clone_and_checkout
# TODO: point to github repo to consume using existing mechanic for packages
# TODO: run a dbt compile to output the consumed manifest.json
# TODO: integrate Doug's consumer ref code
# TODO: what if I included this directly in the deps command? no, keep this separate
# Remember, we aren't doing a real implementation of contracts, just a proof of concept. Therefore, I can create net new scripts knowing they will be thrown away. The goal is understanding the general structure of the code and how it will be used.
class DepsTask(BaseTask):
ConfigType = UnsetProfileConfig
def __init__(self, args, config: UnsetProfileConfig):
super().__init__(args=args, config=config)
def track_package_install(self, package_name: str, source_type: str, version: str) -> None:
# Hub packages do not need to be hashed, as they are public
# Use the string 'local' for local package versions
if source_type == "local":
package_name = dbt.utils.md5(package_name)
version = "local"
elif source_type != "hub":
package_name = dbt.utils.md5(package_name)
version = dbt.utils.md5(version)
dbt.tracking.track_package_install(
self.config,
self.config.args,
{"name": package_name, "source": source_type, "version": version},
)
def run(self):
print("xxxxxxxxxxxxxxxxxxxx")
# system.make_directory(self.config.packages_install_path)
# packages = self.config.packages.packages
# TODO: Locate the dbt_contracts.yml file
project_dir = os.getcwd() # running a dbt project locally
default_directory_location = os.path.join(project_dir, "dbt_contracts.yml")
print(f"default_directory_location: {default_directory_location}")
# TODO: read in the dbt_contracts.yml as a dictionary
with open(default_directory_location, "r") as stream:
contracts_consumed_rendered = load_yaml_text(stream)
print(f"contracts_consumed_rendered: {contracts_consumed_rendered}")
consumer = contracts_consumed_rendered.get("consumer")
print("xxxxxxxxxxxxxxxxxxxx\n")
# TODO: Verify the api private key works(print statement for now: fire_event)
# Will have to create a menu of options such as gcs, s3, API key, etc. to authenticate
contract_validation = {}
for x in consumer:
contract_validation.update({x.get("contract_location"): x.get("credentials")})
print(f'{x.get("name")}: contract credentials verified {green("[OK connection ok]")}')
# TODO: output the consumed code to a `contracts/projects/consumed` directory
contracts_dir = project_dir + "/dbt_contracts"
if not os.path.exists(contracts_dir):
os.mkdir(contracts_dir)
# download the contracts from the contract_location and store them in the contracts_dir
# in the short-term, we will copy the contracts from the local test directory to the contracts_dir
# this contracts.json will consolidate a subset of the manifest.json, catalog.json, run_results.json, sources.json files and then merge that with the consumer's manifest.json, catalog.json(run_results.json, sources.json files are for validating contract requirements only)
dummy_contracts_file_location = "../tests/functional/dbt_contracts/contracts.json"
for x in consumer:
contract_name = x.get("name")
contract_version_expected = x.get("contract_version")
contract_destination = f"{contracts_dir}/{contract_name}-contracts.json"
with open(dummy_contracts_file_location) as json_file:
contract_data = json.load(json_file)
contract_version_actual = contract_data.get("metadata").get("contract_version")
if contract_version_expected == contract_version_actual:
shutil.copyfile(dummy_contracts_file_location, contract_destination)
print(f"Successful contract consumed[{contract_name}]: {contract_destination}")
# TODO: output the consumed contracts.json to a `contracts/consumed` directory within the respective consumed project directory
# TODO: Read in the consumed `contracts.json` to produce a report card in a terminal output
# What's published vs. private nodes?
print(f" Published Nodes: {contract_data.get('contracts').get('published')}")
print(f" Private Nodes: {contract_data.get('contracts').get('private')}")
# What are the contract expectations vs. actuals?
print(
f" Test Coverage: {contract_data.get('contracts').get('requirements').get('test_coverage')} {green('[OK and valid]')}"
)
print(
f" Freshness Coverage: {contract_data.get('contracts').get('requirements').get('freshness_coverage')} {green('[OK and valid]')}"
)
print(
f" Max Upgrade Time Between Versions: {contract_data.get('contracts').get('requirements').get('max_upgrade_time')}"
)
# What permissions do I need to select published nodes?
print(
f" Published Node Permissions: {contract_data.get('contracts').get('permissions')}"
)
# How do I select them?
contract_name = contract_data.get("contracts").get("name")
print(" Published Node Selection:")
print(f" select * from {{{{ ref('{contract_name}','my_first_model') }}}}")
print(f" select * from {{{{ ref('{contract_name}','my_second_model') }}}}")
else:
print(
f"Contract version mismatch, will not consume[{contract_name}]. Expected: {contract_version_expected}, Actual: {contract_version_actual} {red('[Not Compatible]')} \n"
)
# git clone may not be necessary because the contracts.json will contain all the info from the manifest.json and catalog.json
# for x in consumer:
# project_location = x.get("path")
# print(f"project_location: {project_location}")
# clone_and_checkout(repo=project_location, cwd=contracts_dir)
# if not packages:
# fire_event(DepsNoPackagesFound())
# return
# with downloads_directory():
# final_deps = resolve_packages(packages, self.config)
# renderer = DbtProjectYamlRenderer(self.config, self.config.cli_vars)
# packages_to_upgrade = []
# for package in final_deps:
# package_name = package.name
# source_type = package.source_type()
# version = package.get_version()
# fire_event(DepsStartPackageInstall(package_name=package_name))
# package.install(self.config, renderer)
# fire_event(DepsInstallInfo(version_name=package.nice_version_name()))
# if source_type == "hub":
# version_latest = package.get_version_latest()
# if version_latest != version:
# packages_to_upgrade.append(package_name)
# fire_event(DepsUpdateAvailable(version_latest=version_latest))
# else:
# fire_event(DepsUTD())
# if package.get_subdirectory():
# fire_event(DepsListSubdirectory(subdirectory=package.get_subdirectory()))
# self.track_package_install(
# package_name=package_name, source_type=source_type, version=version
# )
# if packages_to_upgrade:
# fire_event(EmptyLine())
# fire_event(DepsNotifyUpdatesAvailable(packages=packages_to_upgrade))
@classmethod
def from_args(cls, args):
# deps needs to move to the project directory, as it does put files
# into the modules directory
move_to_nearest_project_dir(args)
return super().from_args(args)

View File

@@ -204,7 +204,6 @@ def _deep_map_render(
return ret
# TODO: We'll have to re-use this function for dbt_contracts.yml rendering
def deep_map_render(func: Callable[[Any, Tuple[Union[str, int], ...]], Any], value: Any) -> Any:
"""This function renders a nested dictionary derived from a yaml
file. It is used to render dbt_project.yml, profiles.yml, and

View File

@@ -62,6 +62,7 @@ setup(
"dbt-extractor~=0.4.1",
"typing-extensions>=3.7.4",
"werkzeug>=1,<3",
"pathspec~=0.9.0",
# the following are all to match snowflake-connector-python
"requests<3.0.0",
"idna>=2.5,<4",

View File

@@ -0,0 +1,21 @@
{%
set metric_list = [
metric('number_of_people'),
metric('collective_tenure')
]
%}
{% if not execute %}
{% set metric_names = [] %}
{% for m in metric_list %}
{% do metric_names.append(m.metric_name) %}
{% endfor %}
-- this config does nothing, but it lets us check these values
{{ config(metric_names = metric_names) }}
{% endif %}
select 1 as fun

View File

@@ -0,0 +1,17 @@
version: 2
metrics:
- model: "ref('people')"
name: number_of_people
description: Total count of people
label: "Number of people"
calculation_method: count
expression: "*"
timestamp: created_at
time_grains: [day, week, month]
dimensions:
- favorite_color
- loves_dbt
meta:
my_meta: 'replaced'

View File

@@ -52,7 +52,7 @@ class BasePPTest(DBTIntegrationTest):
class MetricsTest(BasePPTest):
@use_profile('postgres')
def test_postgres_env_vars_models(self):
def test_postgres_metrics(self):
self.setup_directories()
# initial run
self.copy_file('test-files/people.sql', 'models/people.sql')
@@ -89,3 +89,18 @@ class MetricsTest(BasePPTest):
expected_depends_on_nodes = ['model.test.people']
self.assertEqual(metric_people.depends_on.nodes, expected_depends_on_nodes)
# Add model referring to metric
self.copy_file('test-files/metric_model_a.sql', 'models/metric_model_a.sql')
results = self.run_dbt(["run"])
manifest = get_manifest()
model_a = manifest.nodes['model.test.metric_model_a']
expected_depends_on_nodes = ['metric.test.number_of_people', 'metric.test.collective_tenure']
self.assertEqual(model_a.depends_on.nodes, expected_depends_on_nodes)
# Then delete a metric
self.copy_file('test-files/people_metrics3.yml', 'models/people_metrics.yml')
with self.assertRaises(CompilationException):
# We use "parse" here and not "run" because we're checking that the CompilationException
# occurs at parse time, not compilation
results = self.run_dbt(["parse"])

View File

@@ -59,7 +59,7 @@ class GraphTest(unittest.TestCase):
# Create file filesystem searcher
self.filesystem_search = patch('dbt.parser.read_files.filesystem_search')
def mock_filesystem_search(project, relative_dirs, extension):
def mock_filesystem_search(project, relative_dirs, extension, ignore_spec):
if 'sql' not in extension:
return []
if 'models' not in relative_dirs:

63
test/unit/test_lib.py Normal file
View File

@@ -0,0 +1,63 @@
import os
import unittest
from unittest import mock
from dbt.contracts.results import RunningStatus
from dbt.lib import compile_sql
from dbt.adapters.postgres import Plugin
from test.unit.utils import clear_plugin, inject_adapter
class MockContext:
def __init__(self, node):
self.timing = []
self.node = mock.MagicMock()
self.node._event_status = {
"node_status": RunningStatus.Started
}
self.node.is_ephemeral_model = True
def noop_ephemeral_result(*args):
return None
class TestSqlCompileRunnerNoIntrospection(unittest.TestCase):
def setUp(self):
self.manifest = {'mock':'manifest'}
self.adapter = Plugin.adapter({})
self.adapter.connection_for = mock.MagicMock()
self.ephemeral_result = lambda: None
inject_adapter(self.adapter, Plugin)
def tearDown(self):
clear_plugin(Plugin)
@mock.patch('dbt.lib._get_operation_node')
@mock.patch('dbt.task.sql.GenericSqlRunner.compile')
@mock.patch('dbt.task.sql.GenericSqlRunner.ephemeral_result', noop_ephemeral_result)
@mock.patch('dbt.task.base.ExecutionContext', MockContext)
def test__compile_and_execute__with_connection(self, mock_compile, mock_get_node):
"""
By default, env var for allowing introspection is true, and calling this
method should defer to the parent method.
"""
mock_get_node.return_value = ({}, None, self.adapter)
compile_sql(self.manifest, 'some/path', None)
mock_compile.assert_called_once_with(self.manifest)
self.adapter.connection_for.assert_called_once()
@mock.patch('dbt.lib._get_operation_node')
@mock.patch('dbt.task.sql.GenericSqlRunner.compile')
@mock.patch('dbt.task.sql.GenericSqlRunner.ephemeral_result', noop_ephemeral_result)
@mock.patch('dbt.task.base.ExecutionContext', MockContext)
def test__compile_and_execute__without_connection(self, mock_compile, mock_get_node):
"""
Ensure that compile is called but does not attempt warehouse connection
"""
with mock.patch.dict(os.environ, {"__DBT_ALLOW_INTROSPECTION": "0"}):
mock_get_node.return_value = ({}, None, self.adapter)
compile_sql(self.manifest, 'some/path', None)
mock_compile.assert_called_once_with(self.manifest)
self.adapter.connection_for.assert_not_called()

View File

@@ -713,6 +713,24 @@ def model(dbt, session):
with self.assertRaises(CompilationException):
self.parser.parse_file(block)
def test_parse_ref_with_non_string(self):
py_code = """
def model(dbt, session):
model_names = ["orders", "customers"]
models = []
for model_name in model_names:
models.extend(dbt.ref(model_name))
return models[0]
"""
block = self.file_block_for(py_code, 'nested/py_model.py')
self.parser.manifest.files[block.file.file_id] = block.file
with self.assertRaises(ParsingException):
self.parser.parse_file(block)
class StaticModelParserTest(BaseParserTest):
def setUp(self):

View File

@@ -4,6 +4,7 @@ import stat
import unittest
import tarfile
import io
import pathspec
from pathlib import Path
from tempfile import mkdtemp, NamedTemporaryFile
@@ -11,6 +12,7 @@ from dbt.exceptions import ExecutableError, WorkingDirectoryError
import dbt.clients.system
class SystemClient(unittest.TestCase):
def setUp(self):
super().setUp()
@@ -151,7 +153,9 @@ class TestFindMatching(unittest.TestCase):
file_path = os.path.dirname(named_file.name)
relative_path = os.path.basename(file_path)
out = dbt.clients.system.find_matching(
self.base_dir, [relative_path], '*.sql'
self.base_dir,
[relative_path],
'*.sql',
)
expected_output = [{
'searched_path': relative_path,
@@ -166,7 +170,9 @@ class TestFindMatching(unittest.TestCase):
file_path = os.path.dirname(named_file.name)
relative_path = os.path.basename(file_path)
out = dbt.clients.system.find_matching(
self.base_dir, [relative_path], '*.sql'
self.base_dir,
[relative_path],
'*.sql'
)
expected_output = [{
'searched_path': relative_path,
@@ -180,7 +186,25 @@ class TestFindMatching(unittest.TestCase):
with NamedTemporaryFile(
prefix='sql-files', suffix='.SQLT', dir=self.tempdir
):
out = dbt.clients.system.find_matching(self.tempdir, [''], '*.sql')
out = dbt.clients.system.find_matching(
self.tempdir,
[''],
'*.sql'
)
self.assertEqual(out, [])
def test_ignore_spec(self):
with NamedTemporaryFile(
prefix='sql-files', suffix='.sql', dir=self.tempdir
):
out = dbt.clients.system.find_matching(
self.tempdir,
[''],
'*.sql',
pathspec.PathSpec.from_lines(
pathspec.patterns.GitWildMatchPattern, "sql-files*".splitlines()
)
)
self.assertEqual(out, [])
def tearDown(self):

View File

@@ -0,0 +1,93 @@
import pytest
from dbt.tests.util import run_dbt
PANDAS_MODEL = """
import pandas as pd
def model(dbt, session):
dbt.config(
materialized="table",
)
df = pd.DataFrame(
{'City': ['Buenos Aires', 'Brasilia', 'Santiago', 'Bogota', 'Caracas'],
'Country': ['Argentina', 'Brazil', 'Chile', 'Colombia', 'Venezuela'],
'Latitude': [-34.58, -15.78, -33.45, 4.60, 10.48],
'Longitude': [-58.66, -47.91, -70.66, -74.08, -66.86]}
)
return df
"""
PYSPARK_MODEL = """
def model(dbt, session):
dbt.config(
materialized="table",
)
df = spark.createDataFrame(
[
("Buenos Aires", "Argentina", -34.58, -58.66),
("Brasilia", "Brazil", -15.78, -47.91),
("Santiago", "Chile", -33.45, -70.66),
("Bogota", "Colombia", 4.60, -74.08),
("Caracas", "Venezuela", 10.48, -66.86),
],
["City", "Country", "Latitude", "Longitude"]
)
return df
"""
PANDAS_ON_SPARK_MODEL = """
import pyspark.pandas as ps
def model(dbt, session):
dbt.config(
materialized="table",
)
df = ps.DataFrame(
{'City': ['Buenos Aires', 'Brasilia', 'Santiago', 'Bogota', 'Caracas'],
'Country': ['Argentina', 'Brazil', 'Chile', 'Colombia', 'Venezuela'],
'Latitude': [-34.58, -15.78, -33.45, 4.60, 10.48],
'Longitude': [-58.66, -47.91, -70.66, -74.08, -66.86]}
)
return df
"""
KOALAS_MODEL = """
import databricks.koalas as ks
def model(dbt, session):
dbt.config(
materialized="table",
)
df = ks.DataFrame(
{'City': ['Buenos Aires', 'Brasilia', 'Santiago', 'Bogota', 'Caracas'],
'Country': ['Argentina', 'Brazil', 'Chile', 'Colombia', 'Venezuela'],
'Latitude': [-34.58, -15.78, -33.45, 4.60, 10.48],
'Longitude': [-58.66, -47.91, -70.66, -74.08, -66.86]}
)
return df
"""
class BasePySparkTests:
@pytest.fixture(scope="class")
def models(self):
return {
"pandas_df.py": PANDAS_MODEL,
"pyspark_df.py": PYSPARK_MODEL,
"pandas_on_spark_df.py": PANDAS_ON_SPARK_MODEL,
"koalas_df.py": KOALAS_MODEL,
}
def test_different_dataframes(self, project):
# test
results = run_dbt(["run"])
assert len(results) == 4

View File

@@ -0,0 +1,22 @@
from dbt.tests.adapter.utils.base_utils import BaseUtils
from dbt.tests.util import run_dbt, check_relations_equal, get_relation_columns
class BaseArrayUtils(BaseUtils):
def assert_columns_equal(self, project, expected_cols, actual_cols):
assert (
expected_cols == actual_cols
), f"Type difference detected: {expected_cols} vs. {actual_cols}"
def test_expected_actual(self, project):
run_dbt(["build"])
# check contents equal
check_relations_equal(project.adapter, ["expected", "actual"])
# check types equal
expected_cols = get_relation_columns(project.adapter, "expected")
actual_cols = get_relation_columns(project.adapter, "actual")
print(f"Expected: {expected_cols}")
print(f"Actual: {actual_cols}")
self.assert_columns_equal(project, expected_cols, actual_cols)

View File

@@ -0,0 +1,12 @@
# array_append
models__array_append_expected_sql = """
select 1 as id, {{ array_construct([1,2,3,4]) }} as array_col union all
select 2 as id, {{ array_construct([4]) }} as array_col
"""
models__array_append_actual_sql = """
select 1 as id, {{ array_append(array_construct([1,2,3]), 4) }} as array_col union all
select 2 as id, {{ array_append(array_construct([]), 4) }} as array_col
"""

View File

@@ -0,0 +1,14 @@
# array_concat
models__array_concat_expected_sql = """
select 1 as id, {{ array_construct([1,2,3,4,5,6]) }} as array_col union all
select 2 as id, {{ array_construct([2]) }} as array_col union all
select 3 as id, {{ array_construct([3]) }} as array_col
"""
models__array_concat_actual_sql = """
select 1 as id, {{ array_concat(array_construct([1,2,3]), array_construct([4,5,6])) }} as array_col union all
select 2 as id, {{ array_concat(array_construct([]), array_construct([2])) }} as array_col union all
select 3 as id, {{ array_concat(array_construct([3]), array_construct([])) }} as array_col
"""

View File

@@ -0,0 +1,12 @@
# array_construct
models__array_construct_expected_sql = """
select 1 as id, {{ array_construct([1,2,3]) }} as array_col union all
select 2 as id, {{ array_construct([]) }} as array_col
"""
models__array_construct_actual_sql = """
select 1 as id, {{ array_construct([1,2,3]) }} as array_col union all
select 2 as id, {{ array_construct([]) }} as array_col
"""

View File

@@ -0,0 +1,19 @@
import pytest
from dbt.tests.adapter.utils.base_array_utils import BaseArrayUtils
from dbt.tests.adapter.utils.fixture_array_append import (
models__array_append_actual_sql,
models__array_append_expected_sql,
)
class BaseArrayAppend(BaseArrayUtils):
@pytest.fixture(scope="class")
def models(self):
return {
"actual.sql": models__array_append_actual_sql,
"expected.sql": models__array_append_expected_sql,
}
class TestArrayAppend(BaseArrayAppend):
pass

View File

@@ -0,0 +1,19 @@
import pytest
from dbt.tests.adapter.utils.base_array_utils import BaseArrayUtils
from dbt.tests.adapter.utils.fixture_array_concat import (
models__array_concat_actual_sql,
models__array_concat_expected_sql,
)
class BaseArrayConcat(BaseArrayUtils):
@pytest.fixture(scope="class")
def models(self):
return {
"actual.sql": models__array_concat_actual_sql,
"expected.sql": models__array_concat_expected_sql,
}
class TestArrayConcat(BaseArrayConcat):
pass

View File

@@ -0,0 +1,19 @@
import pytest
from dbt.tests.adapter.utils.base_array_utils import BaseArrayUtils
from dbt.tests.adapter.utils.fixture_array_construct import (
models__array_construct_actual_sql,
models__array_construct_expected_sql,
)
class BaseArrayConstruct(BaseArrayUtils):
@pytest.fixture(scope="class")
def models(self):
return {
"actual.sql": models__array_construct_actual_sql,
"expected.sql": models__array_construct_expected_sql,
}
class TestArrayConstruct(BaseArrayConstruct):
pass

View File

@@ -0,0 +1,67 @@
import pytest
from datetime import datetime
from datetime import timezone
from datetime import timedelta
from dbt.tests.util import run_dbt
from dbt.tests.util import relation_from_name
models__current_ts_sql = """
select {{ dbt.current_timestamp() }} as current_ts_column
"""
def is_aware(dt: datetime) -> bool:
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
def is_naive(dt: datetime) -> bool:
return not is_aware(dt)
class BaseCurrentTimestamp:
@pytest.fixture(scope="class")
def models(self):
return {
"current_ts.sql": models__current_ts_sql,
}
@pytest.fixture(scope="class")
def current_timestamp(self, project):
run_dbt(["build"])
relation = relation_from_name(project.adapter, "current_ts")
result = project.run_sql(f"select current_ts_column from {relation}", fetch="one")
sql_timestamp = result[0] if result is not None else None
return sql_timestamp
def test_current_timestamp_matches_utc(self, current_timestamp):
sql_timestamp = current_timestamp
now_utc = self.utcnow_matching_type(sql_timestamp)
# Plenty of wiggle room if clocks aren't perfectly sync'd, etc
tolerance = timedelta(minutes=1)
assert (sql_timestamp > (now_utc - tolerance)) and (
sql_timestamp < (now_utc + tolerance)
), f"SQL timestamp {sql_timestamp.isoformat()} is not close enough to Python UTC {now_utc.isoformat()}"
def utcnow_matching_type(self, dt: datetime) -> datetime:
"""
Current UTC datetime with the same timezone-awareness (or naiveness) as the input.
"""
return datetime.now(timezone.utc) if is_aware(dt) else datetime.utcnow()
class BaseCurrentTimestampAware(BaseCurrentTimestamp):
def test_current_timestamp_type(self, current_timestamp):
assert is_aware(current_timestamp)
class BaseCurrentTimestampNaive(BaseCurrentTimestamp):
def test_current_timestamp_type(self, current_timestamp):
assert is_naive(current_timestamp)
# Use either BaseCurrentTimestampAware or BaseCurrentTimestampNaive but not both
class TestCurrentTimestamp(BaseCurrentTimestampAware):
pass

View File

@@ -379,8 +379,14 @@ class JaffleShopProject:
"customers.sql": customers_sql,
"docs.md": docs_md,
"orders.sql": orders_sql,
"ignored_model1.sql": "select 1 as id",
"ignored_model2.sql": "select 1 as id",
"overview.md": overview_md,
"schema.yml": schema_yml,
"ignore_folder": {
"model1.sql": "select 1 as id",
"model2.sql": "select 1 as id",
},
"staging": {
"schema.yml": staging_schema_yml,
"stg_customers.sql": staging_stg_customers_sql,

View File

@@ -1,4 +1,4 @@
from dbt.tests.util import run_dbt, get_manifest, run_dbt_and_capture
from dbt.tests.util import run_dbt, get_manifest, run_dbt_and_capture, write_file
from tests.fixtures.jaffle_shop import JaffleShopProject
@@ -6,6 +6,8 @@ from tests.fixtures.jaffle_shop import JaffleShopProject
class TestBasic(JaffleShopProject):
def test_basic(self, project):
# test .dbtignore works
write_file("models/ignore*.sql\nignore_folder", project.project_root, ".dbtignore")
# Create the data from seeds
results = run_dbt(["seed"])

View File

@@ -1,279 +0,0 @@
{
"metadata": {
"dbt_schema_version": "https://schemas.getdbt.com/dbt/contracts/v1.json",
"dbt_version": "1.3.0b1",
"contract_version": "0.1.0",
"generated_at": "2022-08-17T13:50:25.756889Z",
"invocation_id": "0d8bd1ee-7cf9-4569-87e6-f03be091edf0",
"env": {},
"project_id": "55d83c99b23a2f84fa11d1299c6f9272",
"user_id": "3e8c7b85-8948-45a1-9da8-3c4956a6d938",
"send_anonymous_usage_stats": true,
"adapter_type": "snowflake"
},
"contracts": {
"name": "core-only",
"published": [
"upstream_producer.model.tpch.my_first_model",
"seed.upstream_project.my_first_dbt_seed"
],
"private": [
"model.upstream_project.my_private_model",
"seed.upstream_project.my_private_seed"
],
"requirements": {
"test_coverage": { "expected": 0.8, "actual": 0.85 },
"freshness_coverage": { "expected": 0.8, "actual": 0.85 },
"max_upgrade_time": { "days": 7 }
},
"permissions": ["select: ['user_a', 'user_b']"]
},
"nodes_manifest": {
"upstream_producer.model.tpch.my_first_model": {
"raw_sql": "/*\n Welcome to your first dbt model!\n Did you know that you can also configure models directly within SQL files?\n This will override configurations stated in dbt_project.yml\n Try changing \"table\" to \"view\" below\n*/\n\n\n{{ config(materialized='view') }}\n\n\nwith source_data as (\n\n select 1 as id\n union all\n select 2 as id\n\n)\n\nselect *\nfrom source_data\n\n/*\n Uncomment the line below to remove records with null `id` values\n*/\n\n-- where id is not null",
"resource_type": "model",
"depends_on": { "macros": [], "nodes": [] },
"config": {
"enabled": true,
"alias": null,
"schema": null,
"database": null,
"tags": [],
"meta": {},
"materialized": "view",
"persist_docs": {},
"quoting": {},
"column_types": {},
"full_refresh": null,
"unique_key": null,
"on_schema_change": "ignore",
"post-hook": [],
"pre-hook": []
},
"database": "ANALYTICS",
"schema": "dbt_sung",
"fqn": ["tpch", "demo_examples", "my_first_model"],
"unique_id": "model.tpch.my_first_model",
"package_name": "tpch",
"root_path": "/Users/sung/Desktop/dbt/dbt_env",
"path": "demo_examples/my_first_model.sql",
"original_file_path": "models/demo_examples/my_first_model.sql",
"name": "my_first_model",
"alias": "my_first_model",
"checksum": {
"name": "sha256",
"checksum": "727d4af05e45dc7556410e97db98eb49597270b9f9e0976773a3efe4a40deb47"
},
"tags": [],
"refs": [],
"sources": [],
"description": "",
"columns": {},
"meta": {},
"docs": { "show": true },
"patch_path": null,
"compiled_path": null,
"build_path": null,
"deferred": false,
"unrendered_config": { "materialized": "view" },
"created_at": 1648488021.2328372
},
"upstream_producer.model.tpch.my_second_model": {
"raw_sql": "-- Use the `ref` function to select from other models\n\nselect *\nfrom {{ ref('my_first_model') }}\nwhere id = 2",
"resource_type": "model",
"depends_on": { "macros": [], "nodes": ["model.tpch.my_first_model"] },
"config": {
"enabled": true,
"alias": null,
"schema": null,
"database": null,
"tags": [],
"meta": {},
"materialized": "view",
"persist_docs": {},
"quoting": {},
"column_types": {},
"full_refresh": null,
"unique_key": null,
"on_schema_change": "ignore",
"post-hook": [],
"pre-hook": []
},
"database": "ANALYTICS",
"schema": "dbt_sung",
"fqn": ["tpch", "demo_examples", "my_second_model"],
"unique_id": "model.tpch.my_second_model",
"package_name": "tpch",
"root_path": "/Users/sung/Desktop/dbt/dbt_env",
"path": "demo_examples/my_second_model.sql",
"original_file_path": "models/demo_examples/my_second_model.sql",
"name": "my_second_model",
"alias": "my_second_model",
"checksum": {
"name": "sha256",
"checksum": "f60b137bc59426bbf3b690a71e534735c72cc3f3567c2a4def1f81c578bd0c67"
},
"tags": [],
"refs": [["my_first_model"]],
"sources": [],
"description": "",
"columns": {},
"meta": {},
"docs": { "show": true },
"patch_path": null,
"compiled_path": null,
"build_path": null,
"deferred": false,
"unrendered_config": {},
"created_at": 1648488021.2358248
}
},
"nodes_catalog": {
"model.tpch.my_first_model": {
"metadata": {
"type": "VIEW",
"schema": "DBT_SUNG",
"name": "MY_FIRST_MODEL",
"database": "ANALYTICS",
"comment": null,
"owner": "TRANSFORMER"
},
"columns": {
"ID": { "type": "NUMBER", "index": 1, "name": "ID", "comment": null }
},
"stats": {
"has_stats": {
"id": "has_stats",
"label": "Has Stats?",
"value": false,
"include": false,
"description": "Indicates whether there are statistics for this table"
}
},
"unique_id": "model.tpch.my_first_model"
},
"model.tpch.my_second_model": {
"metadata": {
"type": "VIEW",
"schema": "DBT_SUNG",
"name": "MY_SECOND_MODEL",
"database": "ANALYTICS",
"comment": null,
"owner": "TRANSFORMER"
},
"columns": {
"ID": { "type": "NUMBER", "index": 1, "name": "ID", "comment": null }
},
"stats": {
"has_stats": {
"id": "has_stats",
"label": "Has Stats?",
"value": false,
"include": false,
"description": "Indicates whether there are statistics for this table"
}
},
"unique_id": "model.tpch.my_second_model"
}
},
"nodes_run_results": [
{
"status": "success",
"timing": [
{
"name": "compile",
"started_at": "2022-09-22T21:16:18.812819Z",
"completed_at": "2022-09-22T21:16:18.951459Z"
},
{
"name": "execute",
"started_at": "2022-09-22T21:16:18.966860Z",
"completed_at": "2022-09-22T21:16:18.966873Z"
}
],
"thread_id": "Thread-7",
"execution_time": 0.19610309600830078,
"adapter_response": {},
"message": null,
"failures": null,
"unique_id": "model.tpch.my_first_model"
},
{
"status": "success",
"timing": [
{
"name": "compile",
"started_at": "2022-09-22T21:16:19.684269Z",
"completed_at": "2022-09-22T21:16:19.737776Z"
},
{
"name": "execute",
"started_at": "2022-09-22T21:16:19.813574Z",
"completed_at": "2022-09-22T21:16:19.813583Z"
}
],
"thread_id": "Thread-17",
"execution_time": 0.29882001876831055,
"adapter_response": {},
"message": null,
"failures": null,
"unique_id": "model.tpch.my_second_model"
}
],
"nodes_sources_results": [
{
"unique_id": "source.tpch.tpch_test.lineitem",
"max_loaded_at": "1998-11-29T00:00:00+00:00",
"snapshotted_at": "2022-04-12T14:08:42.647000+00:00",
"max_loaded_at_time_ago_in_s": 737474922.647,
"status": "warn",
"criteria": {
"warn_after": { "count": 6, "period": "hour" },
"error_after": { "count": null, "period": null },
"filter": null
},
"adapter_response": {},
"timing": [
{
"name": "compile",
"started_at": "2022-04-12T14:08:41.488132Z",
"completed_at": "2022-04-12T14:08:41.488138Z"
},
{
"name": "execute",
"started_at": "2022-04-12T14:08:41.489064Z",
"completed_at": "2022-04-12T14:08:43.139148Z"
}
],
"thread_id": "Thread-3",
"execution_time": 1.6662919521331787
},
{
"unique_id": "source.tpch.tpch_test.orders",
"max_loaded_at": "1998-08-02T00:00:00+00:00",
"snapshotted_at": "2022-04-12T14:08:42.656000+00:00",
"max_loaded_at_time_ago_in_s": 747756522.656,
"status": "warn",
"criteria": {
"warn_after": { "count": 6, "period": "hour" },
"error_after": { "count": null, "period": null },
"filter": null
},
"adapter_response": {},
"timing": [
{
"name": "compile",
"started_at": "2022-04-12T14:08:41.475146Z",
"completed_at": "2022-04-12T14:08:41.475159Z"
},
{
"name": "execute",
"started_at": "2022-04-12T14:08:41.475848Z",
"completed_at": "2022-04-12T14:08:43.570703Z"
}
],
"thread_id": "Thread-4",
"execution_time": 2.0986380577087402
}
]
}

View File

@@ -1,90 +0,0 @@
# dbt_contracts.yml
# upstream-only config for illustration
consumer:
core-only: # give the project a plain name to ref
path: git repo OR local subfolder # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/
version: 0.2.0 # Versioning is at the project level, NOT the model level
models:
- ref('fct_orders')
- ref('dim_customers')
requirements: # these can be LESS strict compared to the upstream contract, but NEVER more
- tests
- '>5 run history' # I don't trust data until it's been run a few times, this is tracked statefully within the artifacts_location with run_results.json file counts
api_private_key: 'jioq2hfj28338' # replace with env_var for security TODO: how to store this securely? Do we read this in memory and match public and private api keys?
artifacts_location: 's3://my-bucket/dbt-contracts' # replace with env_var for security, this can be a local path too
# there's definitely a use case for a contracts config to contain BOTH upstream and downstream contracts
# ex: finance-only project that depends on core-only and flows data downstream to marketing-only
# dbt_contracts.yml
# core-only config for illustration
producer:
version: 0.2.0 # version at the top-level only, forced to update if config is different from dbt_contracts.json state file
finance-only: # give the project a plain name to ref
path: git repo OR local subfolder # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/
share_permissions:
full_share: # share everything, code, docs, node, data in lineage
models: # I have 5 models but I only expose data for 4
- ref('fct_orders')
- ref('dim_customers')
- ref('dim_parts')
- ref('dbt_metric')
nodes_only: # share node lineage but not docs, code, or data
models:
+except: # share everything except the below
- ref('stg_sensitive_code')
requirements:
- tests
- freshness
- '>5 run history' # I don't expect downstream users to trust data until it's been run a few times
max_upgrade_time: # enforce how long someone has to upgrade from a previous version to the latest
- date: 11/11/2022 # date to upgrade by, defaults to 12am UTC
version: 0.1.0 # version to upgrade from
api_public_key: 'asfawef3' # replace with env_var for security
artifacts_location: 's3://my-bucket/dbt-contracts' # replace with env_var for security, this can be a local path too AND different from upstream, dbt needs to read those files in memory to compare them
marketing-only: # give the project a plain name to ref
path: git repo OR local subfolder # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/
models: # I have 5 models but I only expose data for 1
- ref('dim_customers')
requirements:
- tests
- freshness
- '>5 run history' # I don't expect downstream users to trust data until it's been run a few times
max_upgrade_time: # enforce how long someone has to upgrade from a previous version to the latest
- days: 0 # No time to upgrade, I'm breaking the contract
api_public_key: 'asfawef3' # replace with env_var for security
artifacts_location: 's3://my-bucket/dbt-contracts' # replace with env_var for security, this can be a local path too AND different from upstream, dbt needs to read those files in memory to compare them
# TODO: how to enforce least privilege across the any config and stricter contracts above?
any: # any downstream project and ref this contract
path: git repo OR local subfolder # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/
models: # I have 5 models but I only expose data for 1
- ref('example_analysis')
requirements:
- tests
max_upgrade_time: # enforce how long someone has to upgrade from a previous version to the latest
- days: 10
api_public_key: 'asfawef3' # replace with env_var for security
artifacts_location: 's3://my-bucket/dbt-contracts' # replace with env_var for security, this can be a local path too AND different from upstream, dbt needs to read those files in memory to compare them
multi-project: # unique config for multi-project contracts
contract_list: # define a list of projects to enforce in a single contract, these have their own configs to map to this list
- operations-only
- sales-only
path: git repo OR local subfolder # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/
models: # I have 5 models but I only expose data for 1
- ref('example_analysis')
requirements:
- tests
max_upgrade_time: # enforce how long someone has to upgrade from a previous version to the latest
- days: 10
api_public_key: 'asfawef3' # replace with env_var for security
artifacts_location: 's3://my-bucket/dbt-contracts' # replace with env_var for security, this can be a local path too AND different from upstream, dbt needs to read those files in memory to compare them
# if the project configs only contain a path, then it searches for the multi-project config
sales-only:
path: git repo OR local subfolder # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/
operations-only:
path: git repo OR local subfolder # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/

View File

@@ -1,16 +0,0 @@
consumer: # akin to a generic top level import like how people work with python
# this presumes the upstream producer solely dictates contract terms
# it is the responsibility of the producer to validate the contract is met
# the consumer is responsible for validating the contract is met to more strigent standards if needed
- name: 'core-only' # give the project a plain name to ref
# path: https://github.com/sungchun12/dbt_bigquery_example.git # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/
contract_version: 0.2.0 # Versioning is at the project level, NOT the model level
contract_location: 's3://core-only-bucket/dbt-contracts'
credentials: '{"aws_access_key_id": "YOUR_ACCESS_KEY_ID", "aws_secret_access_key":"YOUR_SECRET_ACCESS_KEY"}' # replace with env_var for security TODO: how to store this securely? Do we read this in memory and match public and private api keys?
# artifacts_location: argument NOT required as it inherits from producer
- name: 'finance-only' # give the project a plain name to ref
# path: https://github.com/sungchun12/snowflake_dbt_demo_project.git # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/
contract_version: 0.1.0 # Versioning is at the project level, NOT the model level
contract_location: 's3://finance-only-bucket/dbt-contracts'
credentials: '{"aws_access_key_id": "YOUR_ACCESS_KEY_ID", "aws_secret_access_key":"YOUR_SECRET_ACCESS_KEY"}' # replace with env_var for security TODO: how to store this securely? Do we read this in memory and match public and private api keys?
# artifacts_location: argument NOT required as it inherits from producer

View File

@@ -1,52 +0,0 @@
version: 2
models:
- name: dim_customers
contracts:
producer: # this is implied, but should be explicit for clarity
finance-only:
version: 0.1.0
nodes_only: false # optional, default is false
requirements:
test_coverage: # how many of the models are required to be tested
enabled: true # optional, default is false
# threshold: .80 # 80% of the models must be tested, optional, default is 1.0
freshness_coverage: # how many of the sources are required to be fresh
enabled: true # optional, default is false
# threshold: .80 # 80% of the models must be tested, optional, default is 1.0
run_history: 5 # how many sucessful runs are required to be in the run history, look at number of successful run_results.json files in a location and compare to this number
success_only: true # only produce at successful runs else errors for consumers
max_upgrade_time:
days: 10 # how many days can a project be upgraded before it is considered stale
security:
api_public_key: 'asfawef3' # replace with env_var for security
description: Customer dimensions table
docs:
node_color: 'red'
columns:
- name: customer_key
description: Primary key on the customers table
tests:
- unique
- not_null
- name: region
description: region name
tests:
- accepted_values:
values: ['AFRICA','MIDDLE EAST','ASIA','EUROPE','AMERICA']
severity: warn
- name: name
description: customer id
- name: address
description: address of the customer
- name: nation
description: nation name
- name: phone_number
description: phone number of the customer
- name: account_balance
description: '{{ doc("account_balance") }}'
- name: market_segment
description: market segment of the customer
tests:
- unique
- not_null

View File

@@ -1,43 +0,0 @@
/*
Welcome to your first dbt model!
Did you know that you can also configure models directly within SQL files?
This will override configurations stated in dbt_project.yml
Try changing "table" to "view" below
*/
{{ config(materialized='view',
contracts={
"producer": {
"finance-only": {
"version": "0.1.0",
"requirements": {
"test_coverage": { "enabled": true },
"freshness_coverage": { "enabled": true },
"run_history": 5,
"success_only": true,
"max_upgrade_time": { "days": 10 }
},
"security": { "api_public_key": "asfawef3" }
}
}
}
) }}
with source_data as (
select 1 as id
union all
select 2 as id
)
select *
from source_data
/*
Uncomment the line below to remove records with null `id` values
*/
-- where id is not null

View File

@@ -1,142 +0,0 @@
# Name your project! Project names should contain only lowercase characters
# and underscores. A good package name should reflect your organization's
# name or the intended use of these models
name: 'tpch'
version: '1.0.0'
config-version: 2
# This setting configures which "profile" dbt uses for this project.
profile: 'tpch'
# These configurations specify where dbt should look for different types of files.
# The `source-paths` config, for example, states that models in this project can be
# found in the "models/" directory. You probably won't need to change these!
model-paths: ["models"]
analysis-paths: ["analysis"]
test-paths: ["tests"]
seed-paths: ["data"]
macro-paths: ["macros"]
snapshot-paths: ["snapshots"]
#TODO: add api key config here?
# one project = one remote contract artifact location
contract-paths:
local: "contracts"
remote: "s3://my-bucket/dbt-contracts"
target-path: "custom_target_path" # directory which will store compiled SQL files
clean-targets: # directories to be removed by `dbt clean`
- "target"
- "dbt_modules"
on-run-start:
- "alter warehouse transforming set warehouse_size=small;"
- '{{create_udfs()}}' # comment / uncomment this line to build UDFs called in the create_udfs macro
on-run-end:
- "alter warehouse transforming set warehouse_size=xsmall;"
- "{{ grant_all_on_schemas(schemas, 'transformer') }}"
vars:
load_type: 'I'
start_date: '1999-01-01'
test: 'false' # to trigger runs for unit testing - override in a CLI param in testing job
fct_order_items: 'mock_source__fct_order_items' # this is a map for unit testing
dbt_artifacts:
dbt_artifacts_schema: dbt_artifacts_sung # optional, default is 'dbt_artifacts'
dbt_artifacts_table: artifacts # optional, default is 'artifacts'
# Configuring models
# Full documentation: https://docs.getdbt.com/docs/configuring-models
models:
contracts:
producer: # this top-level producer contract for ANY consumers to access
version: 0.1.0
+grants:
select: ['user_a', 'user_b']
models: # I have 5 models but I only expose data for 1
- ref('example_analysis')
- entity('orders')
metrics:
- metric('revenue')
nodes_only:
+except: # share everything except the below
- ref('stg_sensitive_code')
requirements:
test_coverage: # how many of the models are required to be tested
enabled: true # optional, default is false
threshold: .80 # 80% of the models must be tested, optional, default is 1.0
freshness_coverage: # how many of the sources are required to be fresh
enabled: true # optional, default is false
threshold: .80 # 80% of the models must be tested, optional, default is 1.0
run_history: 5 # how many sucessful runs are required to be in the run history, look at number of successful run_results.json files in a location and compare to this number
success_only: true # only produce at successful runs else errors for consumers
max_upgrade_time:
days: 10 # how many days can a project be upgraded before it is considered stale
security:
api_public_key: 'asfawef3' # replace with env_var for security
dbt_artifacts:
+docs:
show: false
+schema: dbt_artifacts_sung
staging:
+schema: dbt_artifacts_sung
tpch:
staging:
+materialized: view
+docs:
# show: false
node_color: "#cd7f32"
marts:
core:
contracts:
producer: # this is implied, but should be explicit for clarity
finance-only:
version: 0.1.0
models: # I have 5 models but I only expose data for 1
- ref('example_analysis')
nodes_only:
+except: # share everything except the below
- ref('stg_sensitive_code')
requirements:
test_coverage: .80 # how many of the models are required to be tested
freshness_coverage: .80 # how many of the sources are required to be fresh
run_history: 5 # how many sucessful runs are required to be in the run history, look at number of successful run_results.json files in a location and compare to this number
success_only: true # only produce at successful runs else errors for consumers
max_upgrade_time:
days: 10 # how many days can a project be upgraded before it is considered stale
security:
api_public_key: 'asfawef3' # replace with env_var for security
multi-project:
sales-only:
path: git repo OR local subfolder # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/
operations-only:
path: git repo OR local subfolder # example: "https://{{env_var('DBT_ENV_SECRET_GIT_CREDENTIAL')}}@github.com/dbt-labs/awesome_repo.git" OR ./models/core/
version: 0.3.0
models: # I have 5 models but I only expose data for 1
- ref('example_analysis')
nodes_only:
+except: # share everything except the below
- ref('stg_sensitive_code')
requirements:
test_coverage: .80 # how many of the models are required to be tested
freshness_coverage: .80 # how many of the sources are required to be fresh
run_history: 5 # how many sucessful runs are required to be in the run history, look at number of successful run_results.json files in a location and compare to this number
success_only: true # only produce at successful runs else errors for consumers
max_upgrade_time:
days: 10 # how many days can a project be upgraded before it is considered stale
security:
api_public_key: 'asfawef3' # replace with env_var for security
materialized: table
+docs:
node_color: "blue"
seeds:
tpch:
snowflake_contract_rates:
+column_types:
effective_date: DATE
rate: NUMBER

View File

@@ -1,55 +0,0 @@
{
"name": "tpch",
"version": "1.0.0",
"config-version": 2,
"profile": "tpch",
"model-paths": ["models"],
"analysis-paths": ["analysis"],
"test-paths": ["tests"],
"seed-paths": ["data"],
"macro-paths": ["macros"],
"snapshot-paths": ["snapshots"],
"target-path": "custom_target_path",
"clean-targets": ["target", "dbt_modules"],
"on-run-start": [
"alter warehouse transforming set warehouse_size=small;",
"{{create_udfs()}}"
],
"on-run-end": [
"alter warehouse transforming set warehouse_size=xsmall;",
"{{ grant_all_on_schemas(schemas, 'transformer') }}"
],
"vars": {
"load_type": "I",
"start_date": "1999-01-01",
"test": "false",
"fct_order_items": "mock_source__fct_order_items",
"dbt_artifacts": {
"dbt_artifacts_schema": "dbt_artifacts_sung",
"dbt_artifacts_table": "artifacts"
}
},
"models": {
"dbt_artifacts": {
"+docs": { "show": false },
"+schema": "dbt_artifacts_sung",
"staging": { "+schema": "dbt_artifacts_sung" }
},
"tpch": {
"staging": {
"+materialized": "view",
"+docs": { "node_color": "#cd7f32" }
},
"marts": {
"core": { "materialized": "table", "+docs": { "node_color": "blue" } }
}
}
},
"seeds": {
"tpch": {
"snowflake_contract_rates": {
"+column_types": { "effective_date": "DATE", "rate": "NUMBER" }
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -138,20 +138,20 @@ class TestRuntimeMaterialization:
project,
):
# initial full-refresh should have no effect
results = run_dbt(["run", "--full-refresh"])
results = run_dbt(["run", "-f"])
assert len(results) == 3
check_relations_equal(project.adapter, ["seed", "view", "incremental", "materialized"])
# adds one record to the incremental model. full-refresh should truncate then re-run
project.run_sql(invalidate_incremental_sql)
results = run_dbt(["run", "--full-refresh"])
results = run_dbt(["run", "-f"])
assert len(results) == 3
check_relations_equal(project.adapter, ["seed", "incremental"])
project.run_sql(update_sql)
results = run_dbt(["run", "--full-refresh"])
results = run_dbt(["run", "-f"])
assert len(results) == 3
check_relations_equal(project.adapter, ["seed", "view", "incremental", "materialized"])
@@ -181,7 +181,7 @@ class TestRuntimeMaterialization:
project.run_sql(create_incremental__dbt_tmp_sql)
assert len(results) == 1
results = run_dbt(["run", "--model", "incremental", "--full-refresh"])
results = run_dbt(["run", "--model", "incremental", "-f"])
assert len(results) == 1
check_table_does_not_exist(project.adapter, "incremental__dbt_tmp")

View File

@@ -188,6 +188,42 @@ class TestInvalidMetricMissingModel:
run_dbt(["run"])
invalid_metrics__missing_expression_yml = """
version: 2
metrics:
- name: number_of_people
label: "Number of people"
model: "ref(people)"
description: Total count of people
calculation_method: count
timestamp: created_at
time_grains: [day, week, month]
dimensions:
- favorite_color
- loves_dbt
meta:
my_meta: 'testing'
"""
class TestInvalidMetricMissingExpression:
@pytest.fixture(scope="class")
def models(self):
return {
"people_metrics.yml": invalid_metrics__missing_expression_yml,
"people.sql": models__people_sql,
}
# tests that we get a ParsingException with a missing expression
def test_simple_metric(
self,
project,
):
# initial run
with pytest.raises(ParsingException):
run_dbt(["run"])
names_with_spaces_metrics_yml = """
version: 2

View File

@@ -0,0 +1,181 @@
import pytest
from dbt.tests.util import run_dbt, write_file, get_manifest
model_one_sql = """
select 1 as fun
"""
schema1_yml = """
version: 2
models:
- name: model_one
metrics:
- name: number_of_people
label: "Number of people"
description: Total count of people
model: "ref('model_one')"
calculation_method: count
expression: "*"
timestamp: created_at
time_grains: [day, week, month]
dimensions:
- favorite_color
- loves_dbt
meta:
my_meta: 'testing'
exposures:
- name: proxy_for_dashboard
description: "My Exposure"
type: "dashboard"
owner:
name: "Dashboard Tester"
email: "tester@dashboard.com"
depends_on:
- ref("model_one")
"""
schema2_yml = """
version: 2
models:
- name: model_one
metrics:
- name: number_of_people
label: "Number of people"
description: Total count of people
config:
enabled: false
model: "ref('model_one')"
calculation_method: count
expression: "*"
timestamp: created_at
time_grains: [day, week, month]
dimensions:
- favorite_color
- loves_dbt
meta:
my_meta: 'testing'
exposures:
- name: proxy_for_dashboard
description: "My Exposure"
config:
enabled: false
type: "dashboard"
owner:
name: "Dashboard Tester"
email: "tester@dashboard.com"
depends_on:
- ref("model_one")
"""
schema3_yml = """
version: 2
models:
- name: model_one
metrics:
- name: number_of_people
label: "Number of people"
description: Total count of people
model: "ref('model_one')"
calculation_method: count
expression: "*"
timestamp: created_at
time_grains: [day, week, month]
dimensions:
- favorite_color
- loves_dbt
meta:
my_meta: 'testing'
"""
schema4_yml = """
version: 2
models:
- name: model_one
exposures:
- name: proxy_for_dashboard
description: "My Exposure"
config:
enabled: false
type: "dashboard"
owner:
name: "Dashboard Tester"
email: "tester@dashboard.com"
depends_on:
- ref("model_one")
"""
class TestDisabled:
@pytest.fixture(scope="class")
def models(self):
return {
"model_one.sql": model_one_sql,
"schema.yml": schema1_yml,
}
def test_pp_disabled(self, project):
expected_exposure = "exposure.test.proxy_for_dashboard"
expected_metric = "metric.test.number_of_people"
run_dbt(["seed"])
results = run_dbt(["run"])
assert len(results) == 1
manifest = get_manifest(project.project_root)
assert expected_exposure in manifest.exposures
assert expected_metric in manifest.metrics
assert expected_exposure not in manifest.disabled
assert expected_metric not in manifest.disabled
# Update schema file with disabled metric and exposure
write_file(schema2_yml, project.project_root, "models", "schema.yml")
results = run_dbt(["--partial-parse", "run"])
assert len(results) == 1
manifest = get_manifest(project.project_root)
assert expected_exposure not in manifest.exposures
assert expected_metric not in manifest.metrics
assert expected_exposure in manifest.disabled
assert expected_metric in manifest.disabled
# Update schema file with enabled metric and exposure
write_file(schema1_yml, project.project_root, "models", "schema.yml")
results = run_dbt(["--partial-parse", "run"])
assert len(results) == 1
manifest = get_manifest(project.project_root)
assert expected_exposure in manifest.exposures
assert expected_metric in manifest.metrics
assert expected_exposure not in manifest.disabled
assert expected_metric not in manifest.disabled
# Update schema file - remove exposure, enable metric
write_file(schema3_yml, project.project_root, "models", "schema.yml")
results = run_dbt(["--partial-parse", "run"])
assert len(results) == 1
manifest = get_manifest(project.project_root)
assert expected_exposure not in manifest.exposures
assert expected_metric in manifest.metrics
assert expected_exposure not in manifest.disabled
assert expected_metric not in manifest.disabled
# Update schema file - add back exposure, remove metric
write_file(schema4_yml, project.project_root, "models", "schema.yml")
results = run_dbt(["--partial-parse", "run"])
assert len(results) == 1
manifest = get_manifest(project.project_root)
assert expected_exposure not in manifest.exposures
assert expected_metric not in manifest.metrics
assert expected_exposure in manifest.disabled
assert expected_metric not in manifest.disabled

View File

@@ -1,7 +1,3 @@
import pytest
from dbt.tests.fixtures.project import write_project_files
wrong_specification_block__schema_yml = """
version: 2
models:
@@ -1245,264 +1241,31 @@ select 1 as "Id"
"""
alt_local_utils__macros__type_timestamp_sql = """
{%- macro type_timestamp() -%}
{{ return(adapter.dispatch('type_timestamp', 'local_utils')()) }}
{%- endmacro -%}
@pytest.fixture(scope="class")
def wrong_specification_block():
return {"schema.yml": wrong_specification_block__schema_yml}
{% macro default__type_timestamp() %}
{{ return(adapter.dispatch('type_timestamp', 'dbt')()) }}
{% endmacro %}
"""
macro_resolution_order_macros__my_custom_test_sql = """
{% test my_custom_test(model) %}
select cast(current_timestamp as {{ dbt.type_timestamp() }})
limit 0
{% endtest %}
"""
@pytest.fixture(scope="class")
def test_context_where_subq_models():
return {
"schema.yml": test_context_where_subq_models__schema_yml,
"model_a.sql": test_context_where_subq_models__model_a_sql,
}
macro_resolution_order_models__my_model_sql = """
select 1 as id
"""
@pytest.fixture(scope="class")
def test_utils():
return {
"dbt_project.yml": test_utils__dbt_project_yml,
"macros": {
"current_timestamp.sql": test_utils__macros__current_timestamp_sql,
"custom_test.sql": test_utils__macros__custom_test_sql,
},
}
@pytest.fixture(scope="class")
def local_dependency():
return {
"dbt_project.yml": local_dependency__dbt_project_yml,
"macros": {"equality.sql": local_dependency__macros__equality_sql},
}
@pytest.fixture(scope="class")
def case_sensitive_models():
return {
"schema.yml": case_sensitive_models__schema_yml,
"lowercase.sql": case_sensitive_models__lowercase_sql,
}
@pytest.fixture(scope="class")
def test_context_macros():
return {
"my_test.sql": test_context_macros__my_test_sql,
"test_my_datediff.sql": test_context_macros__test_my_datediff_sql,
"custom_schema_tests.sql": test_context_macros__custom_schema_tests_sql,
}
@pytest.fixture(scope="class")
def test_context_models_namespaced():
return {
"schema.yml": test_context_models_namespaced__schema_yml,
"model_c.sql": test_context_models_namespaced__model_c_sql,
"model_b.sql": test_context_models_namespaced__model_b_sql,
"model_a.sql": test_context_models_namespaced__model_a_sql,
}
@pytest.fixture(scope="class")
def macros_v2():
return {
"override_get_test_macros_fail": {
"get_test_sql.sql": macros_v2__override_get_test_macros_fail__get_test_sql_sql
},
"macros": {"tests.sql": macros_v2__macros__tests_sql},
"override_get_test_macros": {
"get_test_sql.sql": macros_v2__override_get_test_macros__get_test_sql_sql
},
"custom-configs": {"test.sql": macros_v2__custom_configs__test_sql},
}
@pytest.fixture(scope="class")
def test_context_macros_namespaced():
return {
"my_test.sql": test_context_macros_namespaced__my_test_sql,
"custom_schema_tests.sql": test_context_macros_namespaced__custom_schema_tests_sql,
}
@pytest.fixture(scope="class")
def seeds():
return {"some_seed.csv": seeds__some_seed_csv}
@pytest.fixture(scope="class")
def test_context_models():
return {
"schema.yml": test_context_models__schema_yml,
"model_c.sql": test_context_models__model_c_sql,
"model_b.sql": test_context_models__model_b_sql,
"model_a.sql": test_context_models__model_a_sql,
}
@pytest.fixture(scope="class")
def name_collision():
return {
"schema.yml": name_collision__schema_yml,
"base.sql": name_collision__base_sql,
"base_extension.sql": name_collision__base_extension_sql,
}
@pytest.fixture(scope="class")
def dupe_tests_collide():
return {
"schema.yml": dupe_generic_tests_collide__schema_yml,
"model_a.sql": dupe_generic_tests_collide__model_a,
}
@pytest.fixture(scope="class")
def custom_generic_test_config_custom_macros():
return {
"schema.yml": custom_generic_test_config_custom_macro__schema_yml,
"model_a.sql": custom_generic_test_config_custom_macro__model_a,
}
@pytest.fixture(scope="class")
def custom_generic_test_names():
return {
"schema.yml": custom_generic_test_names__schema_yml,
"model_a.sql": custom_generic_test_names__model_a,
}
@pytest.fixture(scope="class")
def custom_generic_test_names_alt_format():
return {
"schema.yml": custom_generic_test_names_alt_format__schema_yml,
"model_a.sql": custom_generic_test_names_alt_format__model_a,
}
@pytest.fixture(scope="class")
def test_context_where_subq_macros():
return {"custom_generic_test.sql": test_context_where_subq_macros__custom_generic_test_sql}
@pytest.fixture(scope="class")
def invalid_schema_models():
return {
"schema.yml": invalid_schema_models__schema_yml,
"model.sql": invalid_schema_models__model_sql,
}
@pytest.fixture(scope="class")
def all_models():
return {
"render_test_cli_arg_models": {
"schema.yml": models_v2__render_test_cli_arg_models__schema_yml,
"model.sql": models_v2__render_test_cli_arg_models__model_sql,
},
"override_get_test_models": {
"schema.yml": models_v2__override_get_test_models__schema_yml,
"my_model_warning.sql": models_v2__override_get_test_models__my_model_warning_sql,
"my_model_pass.sql": models_v2__override_get_test_models__my_model_pass_sql,
"my_model_failure.sql": models_v2__override_get_test_models__my_model_failure_sql,
},
"models": {
"schema.yml": models_v2__models__schema_yml,
"table_summary.sql": models_v2__models__table_summary_sql,
"table_failure_summary.sql": models_v2__models__table_failure_summary_sql,
"table_disabled.sql": models_v2__models__table_disabled_sql,
"table_failure_null_relation.sql": models_v2__models__table_failure_null_relation_sql,
"table_failure_copy.sql": models_v2__models__table_failure_copy_sql,
"table_copy.sql": models_v2__models__table_copy_sql,
},
"malformed": {
"schema.yml": models_v2__malformed__schema_yml,
"table_summary.sql": models_v2__malformed__table_summary_sql,
"table_copy.sql": models_v2__malformed__table_copy_sql,
},
"override_get_test_models_fail": {
"schema.yml": models_v2__override_get_test_models_fail__schema_yml,
"my_model.sql": models_v2__override_get_test_models_fail__my_model_sql,
},
"custom-configs": {
"schema.yml": models_v2__custom_configs__schema_yml,
"table_copy_another_one.sql": models_v2__custom_configs__table_copy_another_one_sql,
"table_copy.sql": models_v2__custom_configs__table_copy_sql,
"table.copy.with.dots.sql": models_v2__custom_configs__table_copy_with_dots_sql,
},
"render_test_configured_arg_models": {
"schema.yml": models_v2__render_test_configured_arg_models__schema_yml,
"model.sql": models_v2__render_test_configured_arg_models__model_sql,
},
"custom": {
"schema.yml": models_v2__custom__schema_yml,
"table_copy.sql": models_v2__custom__table_copy_sql,
},
"limit_null": {
"schema.yml": models_v2__limit_null__schema_yml,
"table_warning_limit_null.sql": models_v2__limit_null__table_warning_limit_null_sql,
"table_limit_null.sql": models_v2__limit_null__table_limit_null_sql,
"table_failure_limit_null.sql": models_v2__limit_null__table_failure_limit_null_sql,
},
}
@pytest.fixture(scope="class")
def local_utils():
return {
"dbt_project.yml": local_utils__dbt_project_yml,
"macros": {
"datediff.sql": local_utils__macros__datediff_sql,
"current_timestamp.sql": local_utils__macros__current_timestamp_sql,
"custom_test.sql": local_utils__macros__custom_test_sql,
},
}
@pytest.fixture(scope="class")
def ephemeral():
return {
"schema.yml": ephemeral__schema_yml,
"ephemeral.sql": ephemeral__ephemeral_sql,
}
@pytest.fixture(scope="class")
def quote_required_models():
return {
"schema.yml": quote_required_models__schema_yml,
"model_again.sql": quote_required_models__model_again_sql,
"model_noquote.sql": quote_required_models__model_noquote_sql,
"model.sql": quote_required_models__model_sql,
}
@pytest.fixture(scope="class")
def project_files(
project_root,
test_utils,
local_dependency,
test_context_macros,
macros_v2,
test_context_macros_namespaced,
seeds,
test_context_where_subq_macros,
models,
local_utils,
):
write_project_files(project_root, "test_utils", test_utils)
write_project_files(project_root, "local_dependency", local_dependency)
write_project_files(project_root, "test-context-macros", test_context_macros)
write_project_files(project_root, "macros-v2", macros_v2)
write_project_files(
project_root, "test-context-macros-namespaced", test_context_macros_namespaced
)
write_project_files(project_root, "seeds", seeds)
write_project_files(
project_root, "test-context-where-subq-macros", test_context_where_subq_macros
)
write_project_files(project_root, "models", models)
write_project_files(project_root, "local_utils", local_utils)
macro_resolution_order_models__config_yml = """
version: 2
models:
- name: my_model
tests:
- my_custom_test
"""

View File

@@ -5,31 +5,94 @@ import re
from dbt.tests.util import run_dbt, write_file
from dbt.tests.fixtures.project import write_project_files
from tests.fixtures.dbt_integration_project import dbt_integration_project # noqa: F401
from tests.functional.schema_tests.fixtures import ( # noqa: F401
wrong_specification_block,
test_context_where_subq_models,
test_utils,
local_dependency,
case_sensitive_models,
test_context_macros,
test_context_models_namespaced,
macros_v2,
test_context_macros_namespaced,
seeds,
test_context_models,
name_collision,
dupe_tests_collide,
custom_generic_test_config_custom_macros,
custom_generic_test_names,
custom_generic_test_names_alt_format,
test_context_where_subq_macros,
invalid_schema_models,
all_models,
local_utils,
ephemeral,
quote_required_models,
project_files,
from tests.functional.schema_tests.fixtures import (
wrong_specification_block__schema_yml,
test_context_where_subq_models__schema_yml,
test_context_where_subq_models__model_a_sql,
test_utils__dbt_project_yml,
test_utils__macros__current_timestamp_sql,
test_utils__macros__custom_test_sql,
local_dependency__dbt_project_yml,
local_dependency__macros__equality_sql,
case_sensitive_models__schema_yml,
case_sensitive_models__lowercase_sql,
test_context_macros__my_test_sql,
test_context_macros__test_my_datediff_sql,
test_context_macros__custom_schema_tests_sql,
test_context_models_namespaced__schema_yml,
test_context_models_namespaced__model_c_sql,
test_context_models_namespaced__model_b_sql,
test_context_models_namespaced__model_a_sql,
macros_v2__override_get_test_macros_fail__get_test_sql_sql,
macros_v2__macros__tests_sql,
macros_v2__custom_configs__test_sql,
macros_v2__override_get_test_macros__get_test_sql_sql,
test_context_macros_namespaced__my_test_sql,
test_context_macros_namespaced__custom_schema_tests_sql,
seeds__some_seed_csv,
test_context_models__schema_yml,
test_context_models__model_c_sql,
test_context_models__model_b_sql,
test_context_models__model_a_sql,
name_collision__schema_yml,
name_collision__base_sql,
name_collision__base_extension_sql,
dupe_generic_tests_collide__schema_yml,
dupe_generic_tests_collide__model_a,
custom_generic_test_config_custom_macro__schema_yml,
custom_generic_test_config_custom_macro__model_a,
custom_generic_test_names__schema_yml,
custom_generic_test_names__model_a,
custom_generic_test_names_alt_format__schema_yml,
custom_generic_test_names_alt_format__model_a,
test_context_where_subq_macros__custom_generic_test_sql,
invalid_schema_models__schema_yml,
invalid_schema_models__model_sql,
models_v2__models__schema_yml,
models_v2__models__table_summary_sql,
models_v2__models__table_failure_summary_sql,
models_v2__models__table_disabled_sql,
models_v2__models__table_failure_null_relation_sql,
models_v2__models__table_failure_copy_sql,
models_v2__models__table_copy_sql,
models_v2__limit_null__schema_yml,
models_v2__limit_null__table_warning_limit_null_sql,
models_v2__limit_null__table_limit_null_sql,
models_v2__limit_null__table_failure_limit_null_sql,
models_v2__override_get_test_models__schema_yml,
models_v2__override_get_test_models__my_model_warning_sql,
models_v2__override_get_test_models__my_model_pass_sql,
models_v2__override_get_test_models__my_model_failure_sql,
models_v2__override_get_test_models_fail__schema_yml,
models_v2__override_get_test_models_fail__my_model_sql,
models_v2__malformed__schema_yml,
models_v2__malformed__table_summary_sql,
models_v2__malformed__table_copy_sql,
models_v2__custom_configs__schema_yml,
models_v2__custom_configs__table_copy_another_one_sql,
models_v2__custom_configs__table_copy_sql,
models_v2__custom_configs__table_copy_with_dots_sql,
models_v2__custom__schema_yml,
models_v2__custom__table_copy_sql,
models_v2__render_test_cli_arg_models__schema_yml,
models_v2__render_test_cli_arg_models__model_sql,
models_v2__render_test_configured_arg_models__schema_yml,
models_v2__render_test_configured_arg_models__model_sql,
local_utils__dbt_project_yml,
local_utils__macros__datediff_sql,
local_utils__macros__current_timestamp_sql,
local_utils__macros__custom_test_sql,
ephemeral__schema_yml,
ephemeral__ephemeral_sql,
quote_required_models__schema_yml,
quote_required_models__model_again_sql,
quote_required_models__model_noquote_sql,
quote_required_models__model_sql,
case_sensitive_models__uppercase_SQL,
macro_resolution_order_macros__my_custom_test_sql,
macro_resolution_order_models__config_yml,
macro_resolution_order_models__my_model_sql,
alt_local_utils__macros__type_timestamp_sql,
)
from dbt.exceptions import ParsingException, CompilationException
from dbt.contracts.results import TestStatus
@@ -42,8 +105,16 @@ class TestSchemaTests:
project.run_sql_file(os.path.join(project.test_data_dir, "seed_failure.sql"))
@pytest.fixture(scope="class")
def models(self, all_models): # noqa: F811
return all_models["models"]
def models(self):
return {
"schema.yml": models_v2__models__schema_yml,
"table_summary.sql": models_v2__models__table_summary_sql,
"table_failure_summary.sql": models_v2__models__table_failure_summary_sql,
"table_disabled.sql": models_v2__models__table_disabled_sql,
"table_failure_null_relation.sql": models_v2__models__table_failure_null_relation_sql,
"table_failure_copy.sql": models_v2__models__table_failure_copy_sql,
"table_copy.sql": models_v2__models__table_copy_sql,
}
def assertTestFailed(self, result):
assert result.status == "fail"
@@ -117,8 +188,13 @@ class TestLimitedSchemaTests:
project.run_sql_file(os.path.join(project.test_data_dir, "seed.sql"))
@pytest.fixture(scope="class")
def models(self, all_models): # noqa: F811
return all_models["limit_null"]
def models(self):
return {
"schema.yml": models_v2__limit_null__schema_yml,
"table_warning_limit_null.sql": models_v2__limit_null__table_warning_limit_null_sql,
"table_limit_null.sql": models_v2__limit_null__table_limit_null_sql,
"table_failure_limit_null.sql": models_v2__limit_null__table_failure_limit_null_sql,
}
def assertTestFailed(self, result):
assert result.status == "fail"
@@ -163,8 +239,13 @@ class TestLimitedSchemaTests:
class TestDefaultBoolType:
# test with default True/False in get_test_sql macro
@pytest.fixture(scope="class")
def models(self, all_models): # noqa: F811
return all_models["override_get_test_models"]
def models(self):
return {
"schema.yml": models_v2__override_get_test_models__schema_yml,
"my_model_warning.sql": models_v2__override_get_test_models__my_model_warning_sql,
"my_model_pass.sql": models_v2__override_get_test_models__my_model_pass_sql,
"my_model_failure.sql": models_v2__override_get_test_models__my_model_failure_sql,
}
def assertTestFailed(self, result):
assert result.status == "fail"
@@ -207,10 +288,24 @@ class TestDefaultBoolType:
class TestOtherBoolType:
@pytest.fixture(scope="class", autouse=True)
def setUp(self, project_root):
macros_v2_file = {
"override_get_test_macros": {
"get_test_sql.sql": macros_v2__override_get_test_macros__get_test_sql_sql
},
}
write_project_files(project_root, "macros-v2", macros_v2_file)
# test with expected 0/1 in custom get_test_sql macro
@pytest.fixture(scope="class")
def models(self, all_models): # noqa: F811
return all_models["override_get_test_models"]
def models(self):
return {
"schema.yml": models_v2__override_get_test_models__schema_yml,
"my_model_warning.sql": models_v2__override_get_test_models__my_model_warning_sql,
"my_model_pass.sql": models_v2__override_get_test_models__my_model_pass_sql,
"my_model_failure.sql": models_v2__override_get_test_models__my_model_failure_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
@@ -260,10 +355,22 @@ class TestOtherBoolType:
class TestNonBoolType:
@pytest.fixture(scope="class", autouse=True)
def setUp(self, project_root):
macros_v2_file = {
"override_get_test_macros_fail": {
"get_test_sql.sql": macros_v2__override_get_test_macros_fail__get_test_sql_sql
},
}
write_project_files(project_root, "macros-v2", macros_v2_file)
# test with invalid 'x'/'y' in custom get_test_sql macro
@pytest.fixture(scope="class")
def models(self, all_models): # noqa: F811
return all_models["override_get_test_models_fail"]
def models(self):
return {
"schema.yml": models_v2__override_get_test_models_fail__schema_yml,
"my_model.sql": models_v2__override_get_test_models_fail__my_model_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
@@ -291,8 +398,12 @@ class TestMalformedSchemaTests:
project.run_sql_file(os.path.join(project.test_data_dir, "seed.sql"))
@pytest.fixture(scope="class")
def models(self, all_models): # noqa: F811
return all_models["malformed"]
def models(self):
return {
"schema.yml": models_v2__malformed__schema_yml,
"table_summary.sql": models_v2__malformed__table_summary_sql,
"table_copy.sql": models_v2__malformed__table_copy_sql,
}
def test_malformed_schema_will_break_run(
self,
@@ -303,14 +414,22 @@ class TestMalformedSchemaTests:
class TestCustomConfigSchemaTests:
@pytest.fixture(scope="class")
def models(self, all_models): # noqa: F811
return all_models["custom-configs"]
@pytest.fixture(scope="class", autouse=True)
def setUp(self, project):
def setUp(self, project, project_root):
project.run_sql_file(os.path.join(project.test_data_dir, "seed.sql"))
macros_v2_file = {"custom-configs": {"test.sql": macros_v2__custom_configs__test_sql}}
write_project_files(project_root, "macros-v2", macros_v2_file)
@pytest.fixture(scope="class")
def models(self):
return {
"schema.yml": models_v2__custom_configs__schema_yml,
"table_copy_another_one.sql": models_v2__custom_configs__table_copy_another_one_sql,
"table_copy.sql": models_v2__custom_configs__table_copy_sql,
"table.copy.with.dots.sql": models_v2__custom_configs__table_copy_with_dots_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
return {
@@ -333,8 +452,11 @@ class TestCustomConfigSchemaTests:
class TestHooksInTests:
@pytest.fixture(scope="class")
def models(self, ephemeral): # noqa: F811
return ephemeral
def models(self):
return {
"schema.yml": ephemeral__schema_yml,
"ephemeral.sql": ephemeral__ephemeral_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
@@ -359,8 +481,11 @@ class TestHooksInTests:
class TestHooksForWhich:
@pytest.fixture(scope="class")
def models(self, ephemeral): # noqa: F811
return ephemeral
def models(self):
return {
"schema.yml": ephemeral__schema_yml,
"ephemeral.sql": ephemeral__ephemeral_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
@@ -393,6 +518,17 @@ class TestCustomSchemaTests:
write_project_files(project_root, "dbt_integration_project", dbt_integration_project)
project.run_sql_file(os.path.join(project.test_data_dir, "seed.sql"))
local_dependency_files = {
"dbt_project.yml": local_dependency__dbt_project_yml,
"macros": {"equality.sql": local_dependency__macros__equality_sql},
}
write_project_files(project_root, "local_dependency", local_dependency_files)
macros_v2_file = {
"macros": {"tests.sql": macros_v2__macros__tests_sql},
}
write_project_files(project_root, "macros-v2", macros_v2_file)
@pytest.fixture(scope="class")
def packages(self):
return {
@@ -417,8 +553,11 @@ class TestCustomSchemaTests:
}
@pytest.fixture(scope="class")
def models(self, all_models): # noqa: F811
return all_models["custom"]
def models(self):
return {
"schema.yml": models_v2__custom__schema_yml,
"table_copy.sql": models_v2__custom__table_copy_sql,
}
def test_schema_tests(
self,
@@ -443,8 +582,13 @@ class TestCustomSchemaTests:
class TestQuotedSchemaTestColumns:
@pytest.fixture(scope="class")
def models(self, quote_required_models): # noqa: F811
return quote_required_models
def models(self):
return {
"schema.yml": quote_required_models__schema_yml,
"model_again.sql": quote_required_models__model_again_sql,
"model_noquote.sql": quote_required_models__model_noquote_sql,
"model.sql": quote_required_models__model_sql,
}
def test_quote_required_column(
self,
@@ -465,9 +609,19 @@ class TestQuotedSchemaTestColumns:
class TestCliVarsSchemaTests:
@pytest.fixture(scope="class", autouse=True)
def setUp(self, project_root):
macros_v2_file = {
"macros": {"tests.sql": macros_v2__macros__tests_sql},
}
write_project_files(project_root, "macros-v2", macros_v2_file)
@pytest.fixture(scope="class")
def models(self, all_models): # noqa: F811
return all_models["render_test_cli_arg_models"]
def models(self):
return {
"schema.yml": models_v2__render_test_cli_arg_models__schema_yml,
"model.sql": models_v2__render_test_cli_arg_models__model_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
@@ -488,9 +642,19 @@ class TestCliVarsSchemaTests:
class TestConfiguredVarsSchemaTests:
@pytest.fixture(scope="class", autouse=True)
def setUp(self, project_root):
macros_v2_file = {
"macros": {"tests.sql": macros_v2__macros__tests_sql},
}
write_project_files(project_root, "macros-v2", macros_v2_file)
@pytest.fixture(scope="class")
def models(self, all_models): # noqa: F811
return all_models["render_test_configured_arg_models"]
def models(self):
return {
"schema.yml": models_v2__render_test_configured_arg_models__schema_yml,
"model.sql": models_v2__render_test_configured_arg_models__model_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
@@ -512,8 +676,11 @@ class TestConfiguredVarsSchemaTests:
class TestSchemaCaseInsensitive:
@pytest.fixture(scope="class")
def models(self, case_sensitive_models): # noqa: F811
return case_sensitive_models
def models(self):
return {
"schema.yml": case_sensitive_models__schema_yml,
"lowercase.sql": case_sensitive_models__lowercase_sql,
}
@pytest.fixture(scope="class", autouse=True)
def setUP(self, project):
@@ -541,9 +708,33 @@ class TestSchemaCaseInsensitive:
class TestSchemaTestContext:
@pytest.fixture(scope="class", autouse=True)
def setUp(self, project_root):
local_utils_files = {
"dbt_project.yml": local_utils__dbt_project_yml,
"macros": {
"datediff.sql": local_utils__macros__datediff_sql,
"current_timestamp.sql": local_utils__macros__current_timestamp_sql,
"custom_test.sql": local_utils__macros__custom_test_sql,
},
}
write_project_files(project_root, "local_utils", local_utils_files)
test_context_macros_files = {
"my_test.sql": test_context_macros__my_test_sql,
"test_my_datediff.sql": test_context_macros__test_my_datediff_sql,
"custom_schema_tests.sql": test_context_macros__custom_schema_tests_sql,
}
write_project_files(project_root, "test-context-macros", test_context_macros_files)
@pytest.fixture(scope="class")
def models(self, test_context_models): # noqa: F811
return test_context_models
def models(self):
return {
"schema.yml": test_context_models__schema_yml,
"model_c.sql": test_context_models__model_c_sql,
"model_b.sql": test_context_models__model_b_sql,
"model_a.sql": test_context_models__model_a_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
@@ -557,10 +748,7 @@ class TestSchemaTestContext:
def packages(self):
return {"packages": [{"local": "local_utils"}]}
def test_test_context_tests(
self,
project,
):
def test_test_context_tests(self, project):
# This test tests the the TestContext and TestMacroNamespace
# are working correctly
run_dbt(["deps"])
@@ -586,9 +774,43 @@ class TestSchemaTestContext:
class TestSchemaTestContextWithMacroNamespace:
@pytest.fixture(scope="class", autouse=True)
def setUp(self, project_root):
test_utils_files = {
"dbt_project.yml": test_utils__dbt_project_yml,
"macros": {
"current_timestamp.sql": test_utils__macros__current_timestamp_sql,
"custom_test.sql": test_utils__macros__custom_test_sql,
},
}
write_project_files(project_root, "test_utils", test_utils_files)
local_utils_files = {
"dbt_project.yml": local_utils__dbt_project_yml,
"macros": {
"datediff.sql": local_utils__macros__datediff_sql,
"current_timestamp.sql": local_utils__macros__current_timestamp_sql,
"custom_test.sql": local_utils__macros__custom_test_sql,
},
}
write_project_files(project_root, "local_utils", local_utils_files)
test_context_macros_namespaced_file = {
"my_test.sql": test_context_macros_namespaced__my_test_sql,
"custom_schema_tests.sql": test_context_macros_namespaced__custom_schema_tests_sql,
}
write_project_files(
project_root, "test-context-macros-namespaced", test_context_macros_namespaced_file
)
@pytest.fixture(scope="class")
def models(self, test_context_models_namespaced): # noqa: F811
return test_context_models_namespaced
def models(self):
return {
"schema.yml": test_context_models_namespaced__schema_yml,
"model_c.sql": test_context_models_namespaced__model_c_sql,
"model_b.sql": test_context_models_namespaced__model_b_sql,
"model_a.sql": test_context_models_namespaced__model_a_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
@@ -640,8 +862,12 @@ class TestSchemaTestContextWithMacroNamespace:
class TestSchemaTestNameCollision:
@pytest.fixture(scope="class")
def models(self, name_collision): # noqa: F811
return name_collision
def models(self):
return {
"schema.yml": name_collision__schema_yml,
"base.sql": name_collision__base_sql,
"base_extension.sql": name_collision__base_extension_sql,
}
def test_collision_test_names_get_hash(
self,
@@ -666,8 +892,11 @@ class TestSchemaTestNameCollision:
class TestGenericTestsCollide:
@pytest.fixture(scope="class")
def models(self, dupe_tests_collide): # noqa: F811
return dupe_tests_collide
def models(self):
return {
"schema.yml": dupe_generic_tests_collide__schema_yml,
"model_a.sql": dupe_generic_tests_collide__model_a,
}
def test_generic_test_collision(
self,
@@ -681,8 +910,11 @@ class TestGenericTestsCollide:
class TestGenericTestsConfigCustomMacros:
@pytest.fixture(scope="class")
def models(self, custom_generic_test_config_custom_macros): # noqa: F811
return custom_generic_test_config_custom_macros
def models(self):
return {
"schema.yml": custom_generic_test_config_custom_macro__schema_yml,
"model_a.sql": custom_generic_test_config_custom_macro__model_a,
}
def test_generic_test_config_custom_macros(
self,
@@ -696,8 +928,11 @@ class TestGenericTestsConfigCustomMacros:
class TestGenericTestsCustomNames:
@pytest.fixture(scope="class")
def models(self, custom_generic_test_names): # noqa: F811
return custom_generic_test_names
def models(self):
return {
"schema.yml": custom_generic_test_names__schema_yml,
"model_a.sql": custom_generic_test_names__model_a,
}
# users can define custom names for specific instances of generic tests
def test_generic_tests_with_custom_names(
@@ -723,8 +958,11 @@ class TestGenericTestsCustomNames:
class TestGenericTestsCustomNamesAltFormat(TestGenericTestsCustomNames):
@pytest.fixture(scope="class")
def models(self, custom_generic_test_names_alt_format): # noqa: F811
return custom_generic_test_names_alt_format
def models(self):
return {
"schema.yml": custom_generic_test_names_alt_format__schema_yml,
"model_a.sql": custom_generic_test_names_alt_format__model_a,
}
# exactly as above, just alternative format for yaml definition
def test_collision_test_names_get_hash(
@@ -738,8 +976,11 @@ class TestGenericTestsCustomNamesAltFormat(TestGenericTestsCustomNames):
class TestInvalidSchema:
@pytest.fixture(scope="class")
def models(self, invalid_schema_models): # noqa: F811
return invalid_schema_models
def models(self):
return {
"schema.yml": invalid_schema_models__schema_yml,
"model.sql": invalid_schema_models__model_sql,
}
def test_invalid_schema_file(
self,
@@ -752,8 +993,12 @@ class TestInvalidSchema:
class TestWrongSpecificationBlock:
@pytest.fixture(scope="class")
def models(self, wrong_specification_block): # noqa: F811
return wrong_specification_block
def models(self):
return {"schema.yml": wrong_specification_block__schema_yml}
@pytest.fixture(scope="class")
def seeds(self):
return {"some_seed.csv": seeds__some_seed_csv}
def test_wrong_specification_block(
self,
@@ -777,9 +1022,21 @@ class TestWrongSpecificationBlock:
class TestSchemaTestContextWhereSubq:
@pytest.fixture(scope="class", autouse=True)
def setUp(self, project_root):
test_context_where_subq_macros_file = {
"custom_generic_test.sql": test_context_where_subq_macros__custom_generic_test_sql
}
write_project_files(
project_root, "test-context-where-subq-macros", test_context_where_subq_macros_file
)
@pytest.fixture(scope="class")
def models(self, test_context_where_subq_models): # noqa: F811
return test_context_where_subq_models
def models(self):
return {
"schema.yml": test_context_where_subq_models__schema_yml,
"model_a.sql": test_context_where_subq_models__model_a_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
@@ -799,3 +1056,52 @@ class TestSchemaTestContextWhereSubq:
results = run_dbt(["test"])
assert len(results) == 1
class TestCustomSchemaTestMacroResolutionOrder:
@pytest.fixture(scope="class", autouse=True)
def setUp(self, project_root):
alt_local_utils_file = {
"dbt_project.yml": local_utils__dbt_project_yml,
"macros": {
"datediff.sql": alt_local_utils__macros__type_timestamp_sql,
},
}
write_project_files(project_root, "alt_local_utils", alt_local_utils_file)
macros_resolution_order_file = {
"my_custom_test.sql": macro_resolution_order_macros__my_custom_test_sql,
}
write_project_files(
project_root, "macro_resolution_order_macros", macros_resolution_order_file
)
@pytest.fixture(scope="class")
def models(self):
return {
"schema.yml": macro_resolution_order_models__config_yml,
"my_model.sql": macro_resolution_order_models__my_model_sql,
}
@pytest.fixture(scope="class")
def project_config_update(self):
return {
"config-version": 2,
"macro-paths": ["macro_resolution_order_macros"],
}
@pytest.fixture(scope="class")
def packages(self):
return {"packages": [{"local": "alt_local_utils"}]}
def test_macro_resolution_test_namespace(
self,
project,
):
# https://github.com/dbt-labs/dbt-core/issues/5720
# Previously, macros called as 'dbt.some_macro' would not correctly
# resolve to 'some_macro' from the 'dbt' namespace during static analysis,
# if 'some_macro' also existed in an installed package,
# leading to the macro being missing in the TestNamespace
run_dbt(["deps"])
run_dbt(["parse"])