Compare commits

...

24 Commits

Author SHA1 Message Date
Nathaniel May
d342165f6d add exception level 2021-10-29 15:08:07 -04:00
Emily Rockman
a3dc5efda7 context call sites (#4164)
* updated context dir to new structured logging
2021-10-29 10:12:09 -05:00
Nathaniel May
1015b89dbf Initial structured logging work with fire_event (#4137)
add event type modeling and fire_event calls
2021-10-29 09:16:06 -04:00
Nathaniel May
5c9fd07050 init 2021-10-26 13:57:30 -04:00
Gerda Shank
c019a94206 [#4128] Use platform agnostic way to check for tests/generic directory (#4130) 2021-10-25 13:46:49 -04:00
leahwicz
f9bdfa050b Update CHANGELOG.md (#4127) 2021-10-25 13:44:47 -04:00
Gerda Shank
1b35d1aa21 Create more specific tests to debug partial parsing generic tests test (#4129) 2021-10-25 12:20:30 -04:00
Niall Woodward
420ef9cc7b Add _fixed key config back for _choice options in profile_template.yml (#4121) 2021-10-22 17:52:54 +02:00
github-actions[bot]
02fdc2cb9f Bumping version to 1.0.0b2 (#4119)
Co-authored-by: Github Build Bot <buildbot@fishtownanalytics.com>
2021-10-22 11:42:19 -04:00
Joel Labes
f82745fb0c Update git url in setup.py to be dbt-core (#4059)
Tripped over this while following links in #3968
2021-10-22 11:28:24 -04:00
Niall Woodward
3397bdc6a5 dbt init profile_template.yml improvements (#4118)
* Update profile_template.yml to use same syntax as target_options.yml

* Rename target_options to profile_template

* Update profile_template config spec
2021-10-22 17:18:58 +02:00
leahwicz
96e858ac0b Add contributor to changelog (#4120)
* Add contributor to changelog

* Adding another denpendency
2021-10-22 10:45:08 -04:00
dependabot[bot]
f6a98b5674 Update packaging requirement from ~=20.9 to >=20.9,<22.0 in /core (#3532)
Updates the requirements on [packaging](https://github.com/pypa/packaging) to permit the latest version.
- [Release notes](https://github.com/pypa/packaging/releases)
- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/packaging/compare/20.9...21.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-22 10:40:10 -04:00
Joseph H
824f0bf2c0 Upgrade to python3-pip (#4079) 2021-10-22 10:28:29 -04:00
Cor
5648b1c622 Add project name to default search packages (#4114)
* Add project name to default search packages

We prefer macros in the project over the ones in the namespace (package)

* Add change to change log

* Use project_name instead of project

* Raise compilation error if no macros are found

* Update change log line

* Add test for package macro override

* Add JCZuurmond to contributor

* Fix typos

* Add test that should not over ride the package

* Add doc string to tests
2021-10-22 15:15:31 +02:00
Cor
bb1382e576 Allow to overwrite the default test selection (#4116)
It was not possible to overwrite the default test selection for the tox
integration env
2021-10-22 14:57:38 +02:00
Cor
085ea9181f Remove redundant $() (#4115)
This executes the command (on mac OSX at least) thus creating an error
2021-10-22 14:56:11 +02:00
Emily Rockman
eace5b77a7 added test support for databases without boolean types (#4091)
* added support for dbs without boolean types

* catch errors a bit better

* moved changelog entry

* fixed tests and updated exception

* cleaned up bool check

* added positive test, removed self ref
2021-10-21 14:01:41 -05:00
Nathaniel May
1c61bb18e6 Fix static parser tracking again (#4109)
actually fix static parser tracking
2021-10-21 14:35:08 -04:00
Emily Rockman
f79a968a09 add generic tests to test-paths (#4052)
* removed overlooked breakpoint

* first pass

* save progress - singualr tests broken

* fixed to work with both generic and singular tests

* fixed formatting

* added a comment

* change to use /generic subfolder

* fix formatting issues

* fixed bug on code consolidation

* fixed typo

* added test for generic tests

* added changelog entry

* added logic to treat generic tests like macro tests

* add generic test to macro_edges

* fixed generic tests to match unique_ids

* fixed test
2021-10-21 11:42:23 -05:00
Niall Woodward
34c23fe650 Fix init command Windows integration tests (#4107)
* empty

* Trigger tests

* Use os.path.join for filepaths in test cases
2021-10-21 12:08:19 +02:00
Razvan Vacaru
3ae9475655 Fix setup_db.sh by waiting for pg_isready success return. Fixes #3876 (#3908)
* Fix setup_db.sh by waiting for pg_isready success return. Fixes #3876

* restored noaccess and dbtMixedCase creation and updated changelog and contributing md files

* restored root auth commands

* restored creation of dbt schema, aparently this is needed even if docker compose also creates it...

* pr comments: avoid infinite loop and quote variables

* Update changelog

Co-authored-by: Jeremy Cohen <jeremy@dbtlabs.com>
2021-10-21 09:16:28 +02:00
Niall Woodward
11436fed45 dbt init Interactive profile creation (#3625)
* Initial

* Further dev

* Make mypy happy

* Further dev

* Existing tests passing

* Functioning integration test

* Passing integration test

* Integration tests

* Add changelog entry

* Add integration test for init outside of project

* Fall back to target_options.yml when invalid profile_template.yml is provided

* Use built-in yaml with exception of in init

* Remove oyaml and fix tests

* Update dbt_project.yml in test comparison

* Create the profiles directory if it doesn't exist

* Use safe_load

* Update integration test

Co-authored-by: Jeremy Cohen <jeremy@dbtlabs.com>
2021-10-20 18:38:49 +02:00
Nathaniel May
21a7b71657 Fix tracking bug for jinja sampling (#4048)
fix jinja sampling for static parser
2021-10-20 12:38:16 -04:00
60 changed files with 1896 additions and 272 deletions

View File

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

View File

@@ -1,21 +1,34 @@
## dbt-core 1.0.0 (Release TBD)
## dbt-core 1.0.0b2 (October 25, 2021)
### Breaking changes
- Enable `on-run-start` and `on-run-end` hooks for `dbt test`. Add `flags.WHICH` to execution context, representing current task ([#3463](https://github.com/dbt-labs/dbt-core/issues/3463), [#4004](https://github.com/dbt-labs/dbt-core/pull/4004))
### Features
- Normalize global CLI arguments/flags ([#2990](https://github.com/dbt-labs/dbt/issues/2990), [#3839](https://github.com/dbt-labs/dbt/pull/3839))
- Turns on the static parser by default and adds the flag `--no-static-parser` to disable it. ([#3377](https://github.com/dbt-labs/dbt/issues/3377), [#3939](https://github.com/dbt-labs/dbt/pull/3939))
- Generic test FQNs have changed to include the relative path, resource, and column (if applicable) where they are defined. This makes it easier to configure them from the `tests` block in `dbt_project.yml` ([#3259](https://github.com/dbt-labs/dbt/pull/3259), [#3880](https://github.com/dbt-labs/dbt/pull/3880)
- Turn on partial parsing by default ([#3867](https://github.com/dbt-labs/dbt/issues/3867), [#3989](https://github.com/dbt-labs/dbt/issues/3989))
- Add `result:<status>` selectors to automatically rerun failed tests and erroneous models. This makes it easier to rerun failed dbt jobs with a simple selector flag instead of restarting from the beginning or manually running the dbt models in scope. ([#3859](https://github.com/dbt-labs/dbt/issues/3891), [#4017](https://github.com/dbt-labs/dbt/pull/4017))
- `dbt init` is now interactive, generating profiles.yml when run inside existing project ([#3625](https://github.com/dbt-labs/dbt/pull/3625))
### Under the hood
- Fix intermittent errors in partial parsing tests ([#4060](https://github.com/dbt-labs/dbt-core/issues/4060), [#4068](https://github.com/dbt-labs/dbt-core/pull/4068))
- Make finding disabled nodes more consistent ([#4069](https://github.com/dbt-labs/dbt-core/issues/4069), [#4073](https://github.com/dbt-labas/dbt-core/pull/4073))
- Remove connection from `render_with_context` during parsing, thereby removing misleading log message ([#3137](https://github.com/dbt-labs/dbt-core/issues/3137), [#4062](https://github.com/dbt-labas/dbt-core/pull/4062))
- Wait for postgres docker container to be ready in `setup_db.sh`. ([#3876](https://github.com/dbt-labs/dbt-core/issues/3876), [#3908](https://github.com/dbt-labs/dbt-core/pull/3908))
- Prefer macros defined in the project over the ones in a package by default ([#4106](https://github.com/dbt-labs/dbt-core/issues/4106), [#4114](https://github.com/dbt-labs/dbt-core/pull/4114))
- Dependency updates ([#4079](https://github.com/dbt-labs/dbt-core/pull/4079)), ([#3532](https://github.com/dbt-labs/dbt-core/pull/3532)
Contributors:
- [@sungchun12](https://github.com/sungchun12) ([#4017](https://github.com/dbt-labs/dbt/pull/4017))
- [@matt-winkler](https://github.com/matt-winkler) ([#4017](https://github.com/dbt-labs/dbt/pull/4017))
- [@NiallRees](https://github.com/NiallRees) ([#3625](https://github.com/dbt-labs/dbt/pull/3625))
- [@rvacaru](https://github.com/rvacaru) ([#3908](https://github.com/dbt-labs/dbt/pull/3908))
- [@JCZuurmond](https://github.com/jczuurmond) ([#4114](https://github.com/dbt-labs/dbt-core/pull/4114))
- [@ljhopkins2](https://github.com/dbt-labs/dbt-core/pull/4079)
## dbt-core 1.0.0b1 (October 11, 2021)
@@ -29,6 +42,7 @@ Contributors:
- Turns on the static parser by default and adds the flag `--no-static-parser` to disable it. ([#3377](https://github.com/dbt-labs/dbt-core/issues/3377), [#3939](https://github.com/dbt-labs/dbt-core/pull/3939))
- Generic test FQNs have changed to include the relative path, resource, and column (if applicable) where they are defined. This makes it easier to configure them from the `tests` block in `dbt_project.yml` ([#3259](https://github.com/dbt-labs/dbt-core/pull/3259), [#3880](https://github.com/dbt-labs/dbt-core/pull/3880)
- Turn on partial parsing by default ([#3867](https://github.com/dbt-labs/dbt-core/issues/3867), [#3989](https://github.com/dbt-labs/dbt-core/issues/3989))
- Generic test can now be added under a `generic` subfolder in the `test-paths` directory. ([#4052](https://github.com/dbt-labs/dbt-core/pull/4052))
### Fixes
- Add generic tests defined on sources to the manifest once, not twice ([#3347](https://github.com/dbt-labs/dbt/issues/3347), [#3880](https://github.com/dbt-labs/dbt/pull/3880))
@@ -68,6 +82,7 @@ Contributors:
- Performance: Use child_map to find tests for nodes in resolve_graph ([#4012](https://github.com/dbt-labs/dbt/issues/4012), [#4022](https://github.com/dbt-labs/dbt/pull/4022))
- Switch `unique_field` from abstractproperty to optional property. Add docstring ([#4025](https://github.com/dbt-labs/dbt/issues/4025), [#4028](https://github.com/dbt-labs/dbt/pull/4028))
- Include only relational nodes in `database_schema_set` ([#4063](https://github.com/dbt-labs/dbt-core/issues/4063), [#4077](https://github.com/dbt-labs/dbt-core/pull/4077))
- Added support for tests on databases that lack real boolean types. ([#4084](https://github.com/dbt-labs/dbt-core/issues/4084))
Contributors:
- [@ljhopkins2](https://github.com/ljhopkins2) ([#4077](https://github.com/dbt-labs/dbt-core/pull/4077))

View File

@@ -174,8 +174,6 @@ docker-compose up -d database
PGHOST=localhost PGUSER=root PGPASSWORD=password PGDATABASE=postgres bash test/setup_db.sh
```
Note that you may need to run the previous command twice as it does not currently wait for the database to be running before attempting to run commands against it. This will be fixed with [#3876](https://github.com/dbt-labs/dbt-core/issues/3876).
`dbt` uses test credentials specified in a `test.env` file in the root of the repository for non-Postgres databases. This `test.env` file is git-ignored, but please be _extra_ careful to never check in credentials or other sensitive information when developing against `dbt`. To create your `test.env` file, copy the provided sample file, then supply your relevant credentials. This step is only required to use non-Postgres databases.
```

View File

@@ -27,7 +27,7 @@ RUN apt-get update \
&& apt-get install -y \
python \
python-dev \
python-pip \
python3-pip \
python3.6 \
python3.6-dev \
python3-pip \

View File

@@ -1,7 +1,6 @@
import dbt.exceptions
from typing import Any, Dict, Optional
import yaml
import yaml.scanner
# the C version is faster, but it doesn't always exist
try:

View File

@@ -12,7 +12,8 @@ from dbt.clients.yaml_helper import ( # noqa: F401
)
from dbt.contracts.graph.compiled import CompiledResource
from dbt.exceptions import raise_compiler_error, MacroReturn
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.events.functions import fire_event
from dbt.events.types import MacroEventInfo, MacroEventDebug
from dbt.version import __version__ as dbt_version
# These modules are added to the context. Consider alternative
@@ -443,9 +444,9 @@ class BaseContext(metaclass=ContextMeta):
{% endmacro %}"
"""
if info:
logger.info(msg)
fire_event(MacroEventInfo(msg))
else:
logger.debug(msg)
fire_event(MacroEventDebug(msg))
return ''
@contextproperty

View File

@@ -49,7 +49,6 @@ from dbt.exceptions import (
wrapped_exports,
)
from dbt.config import IsFQNResource
from dbt.logger import GLOBAL_LOGGER as logger # noqa
from dbt.node_types import NodeType
from dbt.utils import (
@@ -144,7 +143,7 @@ class BaseDatabaseWrapper:
elif isinstance(namespace, str):
search_packages = self._adapter.config.get_macro_search_order(namespace)
if not search_packages and namespace in self._adapter.config.dependencies:
search_packages = [namespace]
search_packages = [self.config.project_name, namespace]
else:
# Not a string and not None so must be a list
raise CompilationException(
@@ -162,10 +161,10 @@ class BaseDatabaseWrapper:
macro = self._namespace.get_from_package(
package_name, search_name
)
except CompilationException as exc:
raise CompilationException(
f'In dispatch: {exc.msg}',
) from exc
except CompilationException:
# Only raise CompilationException if macro is not found in
# any package
macro = None
if package_name is None:
attempts.append(search_name)

View File

@@ -18,7 +18,8 @@ class ParseFileType(StrEnum):
Model = 'model'
Snapshot = 'snapshot'
Analysis = 'analysis'
Test = 'test'
SingularTest = 'singular_test'
GenericTest = 'generic_test'
Seed = 'seed'
Documentation = 'docs'
Schema = 'schema'
@@ -30,7 +31,8 @@ parse_file_type_to_parser = {
ParseFileType.Model: 'ModelParser',
ParseFileType.Snapshot: 'SnapshotParser',
ParseFileType.Analysis: 'AnalysisParser',
ParseFileType.Test: 'SingularTestParser',
ParseFileType.SingularTest: 'SingularTestParser',
ParseFileType.GenericTest: 'GenericTestParser',
ParseFileType.Seed: 'SeedParser',
ParseFileType.Documentation: 'DocumentationParser',
ParseFileType.Schema: 'SchemaParser',

View File

@@ -291,7 +291,7 @@ def build_node_edges(nodes: List[ManifestNode]):
return _sort_values(forward_edges), _sort_values(backward_edges)
# Build a map of children of macros
# Build a map of children of macros and generic tests
def build_macro_edges(nodes: List[Any]):
forward_edges: Dict[str, List[str]] = {
n.unique_id: [] for n in nodes if n.unique_id.startswith('macro') or n.depends_on.macros

View File

@@ -44,6 +44,11 @@ class UnparsedMacro(UnparsedBaseNode, HasSQL):
resource_type: NodeType = field(metadata={'restrict': [NodeType.Macro]})
@dataclass
class UnparsedGenericTest(UnparsedBaseNode, HasSQL):
resource_type: NodeType = field(metadata={'restrict': [NodeType.Macro]})
@dataclass
class UnparsedNode(UnparsedBaseNode, HasSQL):
name: str

View File

@@ -0,0 +1,9 @@
# Events Module
The Events module is the implmentation for structured logging. These events represent both a programatic interface to dbt processes as well as human-readable messaging in one centralized place. The centralization allows for leveraging mypy to enforce interface invariants across all dbt events, and the distinct type layer allows for decoupling events and libraries such as loggers.
# Using the Events Module
The event module provides types that represent what is happening in dbt in `events.types`. These types are intended to represent an exhaustive list of all things happening within dbt that will need to be logged, streamed, or printed. To fire an event, `events.functions::fire_event` is the entry point to the module from everywhere in dbt.
# Adding a New Event
In `events.types` add a new class that represents the new event. This may be a simple class with no values, or it may be a dataclass with some values to construct downstream messaging. Only include the data necessary to construct this message within this class. You must extend all destinations (e.g. - if your log message belongs on the cli, extend `CliEventABC`) as well as the loglevel this event belongs to.

View File

View File

@@ -0,0 +1,30 @@
import dbt.logger as logger # type: ignore # TODO eventually remove dependency on this logger
from dbt.events.history import EVENT_HISTORY
from dbt.events.types import CliEventABC, Event
# top-level method for accessing the new eventing system
# this is where all the side effects happen branched by event type
# (i.e. - mutating the event history, printing to stdout, logging
# to files, etc.)
def fire_event(e: Event) -> None:
EVENT_HISTORY.append(e)
if isinstance(e, CliEventABC):
if e.level_tag() == 'test':
# TODO after implmenting #3977 send to new test level
logger.GLOBAL_LOGGER.debug(logger.timestamped_line(e.cli_msg()))
elif e.level_tag() == 'debug':
logger.GLOBAL_LOGGER.debug(logger.timestamped_line(e.cli_msg()))
elif e.level_tag() == 'info':
logger.GLOBAL_LOGGER.info(logger.timestamped_line(e.cli_msg()))
elif e.level_tag() == 'warn':
logger.GLOBAL_LOGGER.warning()(logger.timestamped_line(e.cli_msg()))
elif e.level_tag() == 'error':
logger.GLOBAL_LOGGER.error(logger.timestamped_line(e.cli_msg()))
elif e.level_tag() == 'exception':
logger.GLOBAL_LOGGER.exception(logger.timestamped_line(e.cli_msg()))
else:
raise AssertionError(
f"Event type {type(e).__name__} has unhandled level: {e.level_tag()}"
)

View File

@@ -0,0 +1,7 @@
from dbt.events.types import Event
from typing import List
# the global history of events for this session
# TODO this is naive and the memory footprint is likely far too large.
EVENT_HISTORY: List[Event] = []

147
core/dbt/events/types.py Normal file
View File

@@ -0,0 +1,147 @@
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass
# types to represent log levels
# in preparation for #3977
class TestLevel():
def level_tag(self) -> str:
return "test"
class DebugLevel():
def level_tag(self) -> str:
return "debug"
class InfoLevel():
def level_tag(self) -> str:
return "info"
class WarnLevel():
def level_tag(self) -> str:
return "warn"
class ErrorLevel():
def level_tag(self) -> str:
return "error"
class ExceptionLevel():
def level_tag(self) -> str:
return "exception"
# The following classes represent the data necessary to describe a
# particular event to both human readable logs, and machine reliable
# event streams. classes extend superclasses that indicate what
# destinations they are intended for, which mypy uses to enforce
# that the necessary methods are defined.
# top-level superclass for all events
class Event(metaclass=ABCMeta):
# do not define this yourself. inherit it from one of the above level types.
@abstractmethod
def level_tag(self) -> str:
raise Exception("level_tag not implemented for event")
class CliEventABC(Event, metaclass=ABCMeta):
# Solely the human readable message. Timestamps and formatting will be added by the logger.
@abstractmethod
def cli_msg(self) -> str:
raise Exception("cli_msg not implemented for cli event")
class ParsingStart(InfoLevel, CliEventABC):
def cli_msg(self) -> str:
return "Start parsing."
class ParsingCompiling(InfoLevel, CliEventABC):
def cli_msg(self) -> str:
return "Compiling."
class ParsingWritingManifest(InfoLevel, CliEventABC):
def cli_msg(self) -> str:
return "Writing manifest."
class ParsingDone(InfoLevel, CliEventABC):
def cli_msg(self) -> str:
return "Done."
class ManifestDependenciesLoaded(InfoLevel, CliEventABC):
def cli_msg(self) -> str:
return "Dependencies loaded"
class ManifestLoaderCreated(InfoLevel, CliEventABC):
def cli_msg(self) -> str:
return "ManifestLoader created"
class ManifestLoaded(InfoLevel, CliEventABC):
def cli_msg(self) -> str:
return "Manifest loaded"
class ManifestChecked(InfoLevel, CliEventABC):
def cli_msg(self) -> str:
return "Manifest checked"
class ManifestFlatGraphBuilt(InfoLevel, CliEventABC):
def cli_msg(self) -> str:
return "Flat graph built"
@dataclass
class ReportPerformancePath(InfoLevel, CliEventABC):
path: str
def cli_msg(self) -> str:
return f"Performance info: {self.path}"
@dataclass
class MacroEventInfo(InfoLevel, CliEventABC):
msg: str
def cli_msg(self) -> str:
return self.msg
@dataclass
class MacroEventDebug(DebugLevel, CliEventABC):
msg: str
def cli_msg(self) -> str:
return self.msg
# since mypy doesn't run on every file we need to suggest to mypy that every
# class gets instantiated. But we don't actually want to run this code.
# making the conditional `if False` causes mypy to skip it as dead code so
# we need to skirt around that by computing something it doesn't check statically.
#
# TODO remove these lines once we run mypy everywhere.
if 1 == 0:
ParsingStart()
ParsingCompiling()
ParsingWritingManifest()
ParsingDone()
ManifestDependenciesLoaded()
ManifestLoaderCreated()
ManifestLoaded()
ManifestChecked()
ManifestFlatGraphBuilt()
ReportPerformancePath(path='')
MacroEventInfo(msg='')
MacroEventDebug(msg='')

View File

@@ -466,6 +466,15 @@ def invalid_type_error(method_name, arg_name, got_value, expected_type,
got_value=got_value, got_type=got_type))
def invalid_bool_error(got_value, macro_name) -> NoReturn:
"""Raise a CompilationException when an macro expects a boolean but gets some
other value.
"""
msg = ("Macro '{macro_name}' returns '{got_value}'. It is not type 'bool' "
"and cannot not be converted reliably to a bool.")
raise_compiler_error(msg.format(macro_name=macro_name, got_value=got_value))
def ref_invalid_args(model, args) -> NoReturn:
raise_compiler_error(
"ref() takes at most two arguments ({} given)".format(len(args)),

View File

@@ -2,12 +2,12 @@
# 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: 'my_new_project'
name: '{project_name}'
version: '1.0.0'
config-version: 2
# This setting configures which "profile" dbt uses for this project.
profile: 'default'
profile: '{profile_name}'
# These configurations specify where dbt should look for different types of files.
# The `model-paths` config, for example, states that models in this project can be
@@ -30,9 +30,9 @@ clean-targets: # directories to be removed by `dbt clean`
# In this example config, we tell dbt to build all models in the example/ directory
# as tables. These settings can be overridden in the individual model files
# using the `{{ config(...) }}` macro.
# using the `{{{{ config(...) }}}}` macro.
models:
my_new_project:
{project_name}:
# Config indicated by + and applies to all files under models/example/
example:
+materialized: view

View File

@@ -655,8 +655,12 @@ def get_timestamp():
return time.strftime("%H:%M:%S")
def timestamped_line(msg: str) -> str:
return "{} | {}".format(get_timestamp(), msg)
def print_timestamped_line(msg: str, use_color: Optional[str] = None):
if use_color is not None:
msg = dbt.ui.color(msg, use_color)
GLOBAL_LOGGER.info("{} | {}".format(get_timestamp(), msg))
GLOBAL_LOGGER.info(timestamped_line(msg))

View File

@@ -248,7 +248,6 @@ def run_from_args(parsed):
with track_run(task):
results = task.run()
return task, results
@@ -344,20 +343,6 @@ def _build_init_subparser(subparsers, base_subparser):
Initialize a new DBT project.
'''
)
sub.add_argument(
'project_name',
type=str,
help='''
Name of the new project
''',
)
sub.add_argument(
'--adapter',
type=str,
help='''
Write sample profiles.yml for which adapter
''',
)
sub.set_defaults(cls=init_task.InitTask, which='init', rpc_method=None)
return sub

View File

@@ -1,6 +1,7 @@
from .analysis import AnalysisParser # noqa
from .base import Parser, ConfiguredParser # noqa
from .singular_test import SingularTestParser # noqa
from .generic_test import GenericTestParser # noqa
from .docs import DocumentationParser # noqa
from .hooks import HookParser # noqa
from .macros import MacroParser # noqa
@@ -10,6 +11,6 @@ from .seeds import SeedParser # noqa
from .snapshots import SnapshotParser # noqa
from . import ( # noqa
analysis, base, singular_test, docs, hooks, macros, models, schemas,
analysis, base, generic_test, singular_test, docs, hooks, macros, models, schemas,
snapshots
)

View File

@@ -0,0 +1,106 @@
from typing import Iterable, List
import jinja2
from dbt.exceptions import CompilationException
from dbt.clients import jinja
from dbt.contracts.graph.parsed import ParsedGenericTestNode
from dbt.contracts.graph.unparsed import UnparsedMacro
from dbt.contracts.graph.parsed import ParsedMacro
from dbt.contracts.files import SourceFile
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.node_types import NodeType
from dbt.parser.base import BaseParser
from dbt.parser.search import FileBlock
from dbt.utils import MACRO_PREFIX
class GenericTestParser(BaseParser[ParsedGenericTestNode]):
@property
def resource_type(self) -> NodeType:
return NodeType.Macro
@classmethod
def get_compiled_path(cls, block: FileBlock):
return block.path.relative_path
def parse_generic_test(
self, block: jinja.BlockTag, base_node: UnparsedMacro, name: str
) -> ParsedMacro:
unique_id = self.generate_unique_id(name)
return ParsedMacro(
path=base_node.path,
macro_sql=block.full_block,
original_file_path=base_node.original_file_path,
package_name=base_node.package_name,
root_path=base_node.root_path,
resource_type=base_node.resource_type,
name=name,
unique_id=unique_id,
)
def parse_unparsed_generic_test(
self, base_node: UnparsedMacro
) -> Iterable[ParsedMacro]:
try:
blocks: List[jinja.BlockTag] = [
t for t in
jinja.extract_toplevel_blocks(
base_node.raw_sql,
allowed_blocks={'test'},
collect_raw_data=False,
)
if isinstance(t, jinja.BlockTag)
]
except CompilationException as exc:
exc.add_node(base_node)
raise
for block in blocks:
try:
ast = jinja.parse(block.full_block)
except CompilationException as e:
e.add_node(base_node)
raise
# generic tests are structured as macros so we want to count the number of macro blocks
generic_test_nodes = list(ast.find_all(jinja2.nodes.Macro))
if len(generic_test_nodes) != 1:
# things have gone disastrously wrong, we thought we only
# parsed one block!
raise CompilationException(
f'Found multiple generic tests in {block.full_block}, expected 1',
node=base_node
)
generic_test_name = generic_test_nodes[0].name
if not generic_test_name.startswith(MACRO_PREFIX):
continue
name: str = generic_test_name.replace(MACRO_PREFIX, '')
node = self.parse_generic_test(block, base_node, name)
yield node
def parse_file(self, block: FileBlock):
assert isinstance(block.file, SourceFile)
source_file = block.file
assert isinstance(source_file.contents, str)
original_file_path = source_file.path.original_file_path
logger.debug("Parsing {}".format(original_file_path))
# this is really only used for error messages
base_node = UnparsedMacro(
path=original_file_path,
original_file_path=original_file_path,
package_name=self.project.project_name,
raw_sql=source_file.contents,
root_path=self.project.project_root,
resource_type=NodeType.Macro,
)
for node in self.parse_unparsed_generic_test(base_node):
self.manifest.add_macro(block.file, node)

View File

@@ -49,6 +49,7 @@ from dbt.exceptions import (
)
from dbt.parser.base import Parser
from dbt.parser.analysis import AnalysisParser
from dbt.parser.generic_test import GenericTestParser
from dbt.parser.singular_test import SingularTestParser
from dbt.parser.docs import DocumentationParser
from dbt.parser.hooks import HookParser
@@ -277,9 +278,10 @@ class ManifestLoader:
if skip_parsing:
logger.debug("Partial parsing enabled, no changes found, skipping parsing")
else:
# Load Macros
# Load Macros and tests
# We need to parse the macros first, so they're resolvable when
# the other files are loaded
# the other files are loaded. Also need to parse tests, specifically
# generic tests
start_load_macros = time.perf_counter()
self.load_and_parse_macros(project_parser_files)
@@ -379,14 +381,22 @@ class ManifestLoader:
if project.project_name not in project_parser_files:
continue
parser_files = project_parser_files[project.project_name]
if 'MacroParser' not in parser_files:
continue
parser = MacroParser(project, self.manifest)
for file_id in parser_files['MacroParser']:
block = FileBlock(self.manifest.files[file_id])
parser.parse_file(block)
# increment parsed path count for performance tracking
self._perf_info.parsed_path_count = self._perf_info.parsed_path_count + 1
if 'MacroParser' in parser_files:
parser = MacroParser(project, self.manifest)
for file_id in parser_files['MacroParser']:
block = FileBlock(self.manifest.files[file_id])
parser.parse_file(block)
# increment parsed path count for performance tracking
self._perf_info.parsed_path_count = self._perf_info.parsed_path_count + 1
# generic tests hisotrically lived in the macros directoy but can now be nested
# in a /generic directory under /tests so we want to process them here as well
if 'GenericTestParser' in parser_files:
parser = GenericTestParser(project, self.manifest)
for file_id in parser_files['GenericTestParser']:
block = FileBlock(self.manifest.files[file_id])
parser.parse_file(block)
# increment parsed path count for performance tracking
self._perf_info.parsed_path_count = self._perf_info.parsed_path_count + 1
self.build_macro_resolver()
# Look at changed macros and update the macro.depends_on.macros

View File

@@ -12,7 +12,7 @@ from dbt_extractor import ExtractionError, py_extract_from_source # type: ignor
from functools import reduce
from itertools import chain
import random
from typing import Any, Dict, Iterator, List, Optional, Union
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
class ModelParser(SimpleSQLParser[ParsedModelNode]):
@@ -63,14 +63,32 @@ class ModelParser(SimpleSQLParser[ParsedModelNode]):
# top-level declaration of variables
statically_parsed: Optional[Union[str, Dict[str, List[Any]]]] = None
experimental_sample: Optional[Union[str, Dict[str, List[Any]]]] = None
jinja_sample_node = None
jinja_sample_config = None
result = []
exp_sample_node: Optional[ParsedModelNode] = None
exp_sample_config: Optional[ContextConfig] = None
jinja_sample_node: Optional[ParsedModelNode] = None
jinja_sample_config: Optional[ContextConfig] = None
result: List[str] = []
# sample the experimental parser during a normal run
if exp_sample:
# sample the experimental parser only during a normal run
if exp_sample and not flags.USE_EXPERIMENTAL_PARSER:
logger.debug(f"1610: conducting experimental parser sample on {node.path}")
experimental_sample = self.run_experimental_parser(node)
# if the experimental parser succeeded, make a full copy of model parser
# and populate _everything_ into it so it can be compared apples-to-apples
# with a fully jinja-rendered project. This is necessary because the experimental
# parser will likely add features that the existing static parser will fail on
# so comparing those directly would give us bad results. The comparison will be
# conducted after this model has been fully rendered either by the static parser
# or by full jinja rendering
if isinstance(experimental_sample, dict):
model_parser_copy = self.partial_deepcopy()
exp_sample_node = deepcopy(node)
exp_sample_config = deepcopy(config)
model_parser_copy.populate(
exp_sample_node,
exp_sample_config,
experimental_sample
)
# use the experimental parser exclusively if the flag is on
if flags.USE_EXPERIMENTAL_PARSER:
statically_parsed = self.run_experimental_parser(node)
@@ -80,58 +98,61 @@ class ModelParser(SimpleSQLParser[ParsedModelNode]):
# if the static parser succeeded, extract some data in easy-to-compare formats
if isinstance(statically_parsed, dict):
config_call_dict = _get_config_call_dict(statically_parsed)
# since it doesn't need python jinja, fit the refs, sources, and configs
# into the node. Down the line the rest of the node will be updated with
# this information. (e.g. depends_on etc.)
config._config_call_dict = config_call_dict
# this uses the updated config to set all the right things in the node.
# if there are hooks present, it WILL render jinja. Will need to change
# when the experimental parser supports hooks
self.update_parsed_node_config(node, config)
# update the unrendered config with values from the file.
# values from yaml files are in there already
node.unrendered_config.update(dict(statically_parsed['configs']))
# set refs and sources on the node object
node.refs += statically_parsed['refs']
node.sources += statically_parsed['sources']
# configs don't need to be merged into the node
# setting them in config._config_call_dict is sufficient
self.manifest._parsing_info.static_analysis_parsed_path_count += 1
# only sample jinja for the purpose of comparing with the stable static parser
# if we know we don't need to fall back to jinja (i.e. - nothing to compare
# with jinja v jinja).
# This means we skip sampling for 40% of the 1/5000 samples. We could run the
# sampling rng here, but the effect would be the same since we would only roll
# it 40% of the time. So I've opted to keep all the rng code colocated above.
if stable_sample \
and jinja_sample_node is not None \
and jinja_sample_config is not None:
if stable_sample and not flags.USE_EXPERIMENTAL_PARSER:
logger.debug(f"1611: conducting full jinja rendering sample on {node.path}")
# TODO are these deep copies this too expensive?
# TODO does this even mutate anything in `self`???
model_parser_copy = deepcopy(self)
# if this will _never_ mutate anything `self` we could avoid these deep copies,
# but we can't really guarantee that going forward.
model_parser_copy = self.partial_deepcopy()
jinja_sample_node = deepcopy(node)
jinja_sample_config = deepcopy(config)
# rendering mutates the node and the config
super(ModelParser, model_parser_copy) \
.render_update(jinja_sample_node, jinja_sample_config)
# type-level branching to avoid Optional parameters in the
# `_get_stable_sample_result` type signature
if jinja_sample_node is not None and jinja_sample_config is not None:
result.extend(_get_stable_sample_result(
jinja_sample_node,
jinja_sample_config,
node,
config
))
# update the unrendered config with values from the static parser.
# values from yaml files are in there already
self.populate(
node,
config,
statically_parsed
)
# if we took a jinja sample, compare now that the base node has been populated
if jinja_sample_node is not None and jinja_sample_config is not None:
result = _get_stable_sample_result(
jinja_sample_node,
jinja_sample_config,
node,
config
)
# if we took an experimental sample, compare now that the base node has been populated
if exp_sample_node is not None and exp_sample_config is not None:
result = _get_exp_sample_result(
exp_sample_node,
exp_sample_config,
node,
config,
)
self.manifest._parsing_info.static_analysis_parsed_path_count += 1
# if the static parser failed, add the correct messages for tracking
elif isinstance(statically_parsed, str):
if statically_parsed == "cannot_parse":
result += ["01_stable_parser_cannot_parse"]
elif statically_parsed == "has_banned_macro":
result += ["08_has_banned_macro"]
super().render_update(node, config)
logger.debug(
f"1602: parser fallback to jinja rendering on {node.path}"
)
# if the static parser didn't succeed, fall back to jinja
else:
# jinja rendering
@@ -140,14 +161,6 @@ class ModelParser(SimpleSQLParser[ParsedModelNode]):
f"1602: parser fallback to jinja rendering on {node.path}"
)
# if we're sampling the experimental parser, compare for correctness
if exp_sample:
result.extend(_get_exp_sample_result(
experimental_sample,
config_call_dict,
node,
config
))
# only send the tracking event if there is at least one result code
if result:
# fire a tracking event. this fires one event for every sample
@@ -248,10 +261,45 @@ class ModelParser(SimpleSQLParser[ParsedModelNode]):
False
)
# this method updates the model node rendered and unrendered config as well
# as the node object. Used to populate these values when circumventing jinja
# rendering like the static parser.
def populate(
self,
node: ParsedModelNode,
config: ContextConfig,
statically_parsed: Dict[str, Any]
):
# manually fit configs in
config._config_call_dict = _get_config_call_dict(statically_parsed)
# if there are hooks present this, it WILL render jinja. Will need to change
# when the experimental parser supports hooks
self.update_parsed_node_config(node, config)
# update the unrendered config with values from the file.
# values from yaml files are in there already
node.unrendered_config.update(dict(statically_parsed['configs']))
# set refs and sources on the node object
node.refs += statically_parsed['refs']
node.sources += statically_parsed['sources']
# configs don't need to be merged into the node because they
# are read from config._config_call_dict
# the manifest is often huge so this method avoids deepcopying it
def partial_deepcopy(self):
return ModelParser(
deepcopy(self.project),
self.manifest,
deepcopy(self.root_project)
)
# pure function. safe to use elsewhere, but unlikely to be useful outside this file.
def _get_config_call_dict(
static_parser_result: Dict[str, List[Any]]
static_parser_result: Dict[str, Any]
) -> Dict[str, Any]:
config_call_dict: Dict[str, Any] = {}
@@ -277,63 +325,18 @@ def _shift_sources(
# returns a list of string codes to be sent as a tracking event
def _get_exp_sample_result(
sample_output: Optional[Union[str, Dict[str, Any]]],
config_call_dict: Dict[str, Any],
sample_node: ParsedModelNode,
sample_config: ContextConfig,
node: ParsedModelNode,
config: ContextConfig
) -> List[str]:
result: List[str] = []
# experimental parser didn't run
if sample_output is None:
result += ["09_experimental_parser_skipped"]
# experimental parser couldn't parse
elif isinstance(sample_output, str):
if sample_output == "cannot_parse":
result += ["01_experimental_parser_cannot_parse"]
elif sample_output == "has_banned_macro":
result += ["08_has_banned_macro"]
else:
# look for false positive configs
for k in config_call_dict.keys():
if k not in config._config_call_dict:
result += ["02_false_positive_config_value"]
break
result: List[Tuple[int, str]] = _get_sample_result(sample_node, sample_config, node, config)
# look for missed configs
for k in config._config_call_dict.keys():
if k not in config_call_dict:
result += ["03_missed_config_value"]
break
def process(codemsg):
code, msg = codemsg
return f"0{code}_experimental_{msg}"
# look for false positive sources
for s in sample_output['sources']:
if s not in node.sources:
result += ["04_false_positive_source_value"]
break
# look for missed sources
for s in node.sources:
if s not in sample_output['sources']:
result += ["05_missed_source_value"]
break
# look for false positive refs
for r in sample_output['refs']:
if r not in node.refs:
result += ["06_false_positive_ref_value"]
break
# look for missed refs
for r in node.refs:
if r not in sample_output['refs']:
result += ["07_missed_ref_value"]
break
# if there are no errors, return a success value
if not result:
result = ["00_exact_match"]
return result
return list(map(process, result))
# returns a list of string codes to be sent as a tracking event
@@ -343,45 +346,62 @@ def _get_stable_sample_result(
node: ParsedModelNode,
config: ContextConfig
) -> List[str]:
result: List[str] = []
result: List[Tuple[int, str]] = _get_sample_result(sample_node, sample_config, node, config)
def process(codemsg):
code, msg = codemsg
return f"8{code}_stable_{msg}"
return list(map(process, result))
# returns a list of string codes that need a single digit prefix to be prepended
# before being sent as a tracking event
def _get_sample_result(
sample_node: ParsedModelNode,
sample_config: ContextConfig,
node: ParsedModelNode,
config: ContextConfig
) -> List[Tuple[int, str]]:
result: List[Tuple[int, str]] = []
# look for false positive configs
for k in config._config_call_dict:
if k not in config._config_call_dict:
result += ["82_stable_false_positive_config_value"]
for k in sample_config._config_call_dict.keys():
if k not in config._config_call_dict.keys():
result += [(2, "false_positive_config_value")]
break
# look for missed configs
for k in config._config_call_dict.keys():
if k not in sample_config._config_call_dict.keys():
result += ["83_stable_missed_config_value"]
result += [(3, "missed_config_value")]
break
# look for false positive sources
for s in sample_node.sources:
if s not in node.sources:
result += ["84_sample_false_positive_source_value"]
result += [(4, "false_positive_source_value")]
break
# look for missed sources
for s in node.sources:
if s not in sample_node.sources:
result += ["85_sample_missed_source_value"]
result += [(5, "missed_source_value")]
break
# look for false positive refs
for r in sample_node.refs:
if r not in node.refs:
result += ["86_sample_false_positive_ref_value"]
result += [(6, "false_positive_ref_value")]
break
# look for missed refs
for r in node.refs:
if r not in sample_node.refs:
result += ["87_stable_missed_ref_value"]
result += [(7, "missed_ref_value")]
break
# if there are no errors, return a success value
if not result:
result = ["80_stable_exact_match"]
result = [(0, "exact_match")]
return result

View File

@@ -13,7 +13,12 @@ mssat_files = (
ParseFileType.Seed,
ParseFileType.Snapshot,
ParseFileType.Analysis,
ParseFileType.Test,
ParseFileType.SingularTest,
)
mg_files = (
ParseFileType.Macro,
ParseFileType.GenericTest,
)
@@ -88,7 +93,7 @@ class PartialParsing:
if self.saved_files[file_id].parse_file_type == ParseFileType.Schema:
deleted_schema_files.append(file_id)
else:
if self.saved_files[file_id].parse_file_type == ParseFileType.Macro:
if self.saved_files[file_id].parse_file_type in mg_files:
changed_or_deleted_macro_file = True
deleted.append(file_id)
@@ -106,7 +111,7 @@ class PartialParsing:
raise Exception(f"Serialization failure for {file_id}")
changed_schema_files.append(file_id)
else:
if self.saved_files[file_id].parse_file_type == ParseFileType.Macro:
if self.saved_files[file_id].parse_file_type in mg_files:
changed_or_deleted_macro_file = True
changed.append(file_id)
file_diff = {
@@ -213,7 +218,7 @@ class PartialParsing:
self.deleted_manifest.files[file_id] = self.saved_manifest.files.pop(file_id)
# macros
if saved_source_file.parse_file_type == ParseFileType.Macro:
if saved_source_file.parse_file_type in mg_files:
self.delete_macro_file(saved_source_file, follow_references=True)
# docs
@@ -229,7 +234,7 @@ class PartialParsing:
if new_source_file.parse_file_type in mssat_files:
self.update_mssat_in_saved(new_source_file, old_source_file)
elif new_source_file.parse_file_type == ParseFileType.Macro:
elif new_source_file.parse_file_type in mg_files:
self.update_macro_in_saved(new_source_file, old_source_file)
elif new_source_file.parse_file_type == ParseFileType.Documentation:
self.update_doc_in_saved(new_source_file, old_source_file)

View File

@@ -1,3 +1,4 @@
import pathlib
from dbt.clients.system import load_file_contents
from dbt.contracts.files import (
FilePath, ParseFileType, SourceFile, FileHash, AnySourceFile, SchemaSourceFile
@@ -93,7 +94,13 @@ def get_source_files(project, paths, extension, parse_file_type, saved_files):
for fp in fp_list:
if parse_file_type == ParseFileType.Seed:
fb_list.append(load_seed_source_file(fp, project.project_name))
# singular tests live in /tests but only generic tests live
# in /tests/generic so we want to skip those
else:
if parse_file_type == ParseFileType.SingularTest:
path = pathlib.Path(fp.relative_path)
if path.parts[0] == 'generic':
continue
file = load_source_file(fp, parse_file_type, project.project_name, saved_files)
# only append the list if it has contents. added to fix #3568
if file:
@@ -137,7 +144,13 @@ def read_files(project, files, parser_files, saved_files):
)
project_files['SingularTestParser'] = read_files_for_parser(
project, files, project.test_paths, '.sql', ParseFileType.Test, saved_files
project, files, project.test_paths, '.sql', ParseFileType.SingularTest, saved_files
)
# all generic tests within /tests must be nested under a /generic subfolder
project_files['GenericTestParser'] = read_files_for_parser(
project, files, ["{}{}".format(test_path, '/generic') for test_path in project.test_paths],
'.sql', ParseFileType.GenericTest, saved_files
)
project_files['SeedParser'] = read_files_for_parser(

View File

@@ -1,5 +1,12 @@
import copy
import os
from pathlib import Path
import re
import shutil
from typing import Optional
import yaml
import click
import dbt.config
import dbt.clients.system
@@ -11,7 +18,7 @@ from dbt.logger import GLOBAL_LOGGER as logger
from dbt.include.starter_project import PACKAGE_PATH as starter_project_directory
from dbt.task.base import BaseTask
from dbt.task.base import BaseTask, move_to_nearest_project_dir
DOCS_URL = 'https://docs.getdbt.com/docs/configure-your-profile'
SLACK_URL = 'https://community.getdbt.com/'
@@ -20,11 +27,7 @@ SLACK_URL = 'https://community.getdbt.com/'
IGNORE_FILES = ["__init__.py", "__pycache__"]
ON_COMPLETE_MESSAGE = """
Your new dbt project "{project_name}" was created! If this is your first time
using dbt, you'll need to set up your profiles.yml file -- this file will tell dbt how
to connect to your database. You can find this file by running:
{open_cmd} {profiles_path}
Your new dbt project "{project_name}" was created!
For more information on how to configure the profiles.yml file,
please consult the dbt documentation here:
@@ -40,6 +43,15 @@ Need help? Don't hesitate to reach out to us via GitHub issues or on Slack:
Happy modeling!
"""
# https://click.palletsprojects.com/en/8.0.x/api/?highlight=float#types
click_type_mapping = {
"string": click.STRING,
"int": click.INT,
"float": click.FLOAT,
"bool": click.BOOL,
None: None
}
class InitTask(BaseTask):
def copy_starter_repo(self, project_name):
@@ -47,33 +59,52 @@ class InitTask(BaseTask):
shutil.copytree(starter_project_directory, project_name,
ignore=shutil.ignore_patterns(*IGNORE_FILES))
def create_profiles_dir(self, profiles_dir):
if not os.path.exists(profiles_dir):
def create_profiles_dir(self, profiles_dir: str) -> bool:
"""Create the user's profiles directory if it doesn't already exist."""
profiles_path = Path(profiles_dir)
if not profiles_path.exists():
msg = "Creating dbt configuration folder at {}"
logger.info(msg.format(profiles_dir))
dbt.clients.system.make_directory(profiles_dir)
return True
return False
def create_profiles_file(self, profiles_file, sample_adapter):
def create_profile_from_sample(self, adapter: str, profile_name: str):
"""Create a profile entry using the adapter's sample_profiles.yml
Renames the profile in sample_profiles.yml to match that of the project."""
# Line below raises an exception if the specified adapter is not found
load_plugin(sample_adapter)
adapter_path = get_include_paths(sample_adapter)[0]
sample_profiles_path = adapter_path / 'sample_profiles.yml'
load_plugin(adapter)
adapter_path = get_include_paths(adapter)[0]
sample_profiles_path = adapter_path / "sample_profiles.yml"
if not sample_profiles_path.exists():
logger.debug(f"No sample profile found for {sample_adapter}, skipping")
return False
logger.debug(f"No sample profile found for {adapter}.")
else:
with open(sample_profiles_path, "r") as f:
sample_profile = f.read()
sample_profile_name = list(yaml.safe_load(sample_profile).keys())[0]
# Use a regex to replace the name of the sample_profile with
# that of the project without losing any comments from the sample
sample_profile = re.sub(
f"^{sample_profile_name}:",
f"{profile_name}:",
sample_profile
)
profiles_filepath = Path(flags.PROFILES_DIR) / Path("profiles.yml")
if profiles_filepath.exists():
with open(profiles_filepath, "a") as f:
f.write("\n" + sample_profile)
else:
with open(profiles_filepath, "w") as f:
f.write(sample_profile)
logger.info(
f"Profile {profile_name} written to {profiles_filepath} "
"using target's sample configuration. Once updated, you'll be able to "
"start developing with dbt."
)
if not os.path.exists(profiles_file):
msg = "With sample profiles.yml for {}"
logger.info(msg.format(sample_adapter))
shutil.copyfile(sample_profiles_path, profiles_file)
return True
return False
def get_addendum(self, project_name, profiles_path):
def get_addendum(self, project_name: str, profiles_path: str) -> str:
open_cmd = dbt.clients.system.open_dir_cmd()
return ON_COMPLETE_MESSAGE.format(
@@ -84,29 +115,212 @@ class InitTask(BaseTask):
slack_url=SLACK_URL
)
def generate_target_from_input(
self,
profile_template: dict,
target: dict = {}
) -> dict:
"""Generate a target configuration from profile_template and user input.
"""
profile_template_local = copy.deepcopy(profile_template)
for key, value in profile_template_local.items():
if key.startswith("_choose"):
choice_type = key[8:].replace("_", " ")
option_list = list(value.keys())
prompt_msg = "\n".join([
f"[{n+1}] {v}" for n, v in enumerate(option_list)
]) + f"\nDesired {choice_type} option (enter a number)"
numeric_choice = click.prompt(prompt_msg, type=click.INT)
choice = option_list[numeric_choice - 1]
# Complete the chosen option's values in a recursive call
target = self.generate_target_from_input(
profile_template_local[key][choice], target
)
else:
if key.startswith("_fixed"):
# _fixed prefixed keys are not presented to the user
target[key[7:]] = value
else:
hide_input = value.get("hide_input", False)
default = value.get("default", None)
hint = value.get("hint", None)
type = click_type_mapping[value.get("type", None)]
text = key + (f" ({hint})" if hint else "")
target[key] = click.prompt(
text,
default=default,
hide_input=hide_input,
type=type
)
return target
def get_profile_name_from_current_project(self) -> str:
"""Reads dbt_project.yml in the current directory to retrieve the
profile name.
"""
with open("dbt_project.yml") as f:
dbt_project = yaml.safe_load(f)
return dbt_project["profile"]
def write_profile(
self, profile: dict, profile_name: str
):
"""Given a profile, write it to the current project's profiles.yml.
This will overwrite any profile with a matching name."""
# Create the profile directory if it doesn't exist
profiles_filepath = Path(flags.PROFILES_DIR) / Path("profiles.yml")
if profiles_filepath.exists():
with open(profiles_filepath, "r+") as f:
profiles = yaml.safe_load(f) or {}
profiles[profile_name] = profile
f.seek(0)
yaml.dump(profiles, f)
f.truncate()
else:
profiles = {profile_name: profile}
with open(profiles_filepath, "w") as f:
yaml.dump(profiles, f)
def create_profile_from_profile_template(self, profile_template: dict, profile_name: str):
"""Create and write a profile using the supplied profile_template."""
initial_target = profile_template.get('fixed', {})
prompts = profile_template.get('prompts', {})
target = self.generate_target_from_input(prompts, initial_target)
profile = {
"outputs": {
"dev": target
},
"target": "dev"
}
self.write_profile(profile, profile_name)
def create_profile_from_target(self, adapter: str, profile_name: str):
"""Create a profile without defaults using target's profile_template.yml if available, or
sample_profiles.yml as a fallback."""
# Line below raises an exception if the specified adapter is not found
load_plugin(adapter)
adapter_path = get_include_paths(adapter)[0]
profile_template_path = adapter_path / "profile_template.yml"
if profile_template_path.exists():
with open(profile_template_path) as f:
profile_template = yaml.safe_load(f)
self.create_profile_from_profile_template(profile_template, profile_name)
profiles_filepath = Path(flags.PROFILES_DIR) / Path("profiles.yml")
logger.info(
f"Profile {profile_name} written to {profiles_filepath} using target's "
"profile_template.yml and your supplied values. Run 'dbt debug' to "
"validate the connection."
)
else:
# For adapters without a profile_template.yml defined, fallback on
# sample_profiles.yml
self.create_profile_from_sample(adapter, profile_name)
def check_if_can_write_profile(self, profile_name: Optional[str] = None) -> bool:
"""Using either a provided profile name or that specified in dbt_project.yml,
check if the profile already exists in profiles.yml, and if so ask the
user whether to proceed and overwrite it."""
profiles_file = Path(flags.PROFILES_DIR) / Path("profiles.yml")
if not profiles_file.exists():
return True
profile_name = (
profile_name or self.get_profile_name_from_current_project()
)
with open(profiles_file, "r") as f:
profiles = yaml.safe_load(f) or {}
if profile_name in profiles.keys():
response = click.confirm(
f"The profile {profile_name} already exists in "
f"{profiles_file}. Continue and overwrite it?"
)
return response
else:
return True
def create_profile_using_project_profile_template(self, profile_name):
"""Create a profile using the project's profile_template.yml"""
with open("profile_template.yml") as f:
profile_template = yaml.safe_load(f)
self.create_profile_from_profile_template(profile_template, profile_name)
profiles_filepath = Path(flags.PROFILES_DIR) / Path("profiles.yml")
logger.info(
f"Profile {profile_name} written to {profiles_filepath} using project's "
"profile_template.yml and your supplied values. Run 'dbt debug' to "
"validate the connection."
)
def ask_for_adapter_choice(self) -> str:
"""Ask the user which adapter (database) they'd like to use."""
available_adapters = list(_get_adapter_plugin_names())
prompt_msg = (
"Which database would you like to use?\n" +
"\n".join([f"[{n+1}] {v}" for n, v in enumerate(available_adapters)]) +
"\n\n(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)" +
"\n\nEnter a number"
)
numeric_choice = click.prompt(prompt_msg, type=click.INT)
return available_adapters[numeric_choice - 1]
def run(self):
project_dir = self.args.project_name
sample_adapter = self.args.adapter
if not sample_adapter:
try:
# pick first one available, often postgres
sample_adapter = next(_get_adapter_plugin_names())
except StopIteration:
logger.debug("No adapters installed, skipping")
"""Entry point for the init task."""
profiles_dir = flags.PROFILES_DIR
profiles_file = os.path.join(profiles_dir, 'profiles.yml')
self.create_profiles_dir(profiles_dir)
if sample_adapter:
self.create_profiles_file(profiles_file, sample_adapter)
if os.path.exists(project_dir):
raise RuntimeError("directory {} already exists!".format(
project_dir
))
try:
move_to_nearest_project_dir(self.args)
in_project = True
except dbt.exceptions.RuntimeException:
in_project = False
self.copy_starter_repo(project_dir)
if in_project:
# When dbt init is run inside an existing project,
# just setup the user's profile.
logger.info("Setting up your profile.")
profile_name = self.get_profile_name_from_current_project()
# If a profile_template.yml exists in the project root, that effectively
# overrides the profile_template.yml for the given target.
profile_template_path = Path("profile_template.yml")
if profile_template_path.exists():
try:
# This relies on a valid profile_template.yml from the user,
# so use a try: except to fall back to the default on failure
self.create_profile_using_project_profile_template(profile_name)
return
except Exception:
logger.info("Invalid profile_template.yml in project.")
if not self.check_if_can_write_profile(profile_name=profile_name):
return
adapter = self.ask_for_adapter_choice()
self.create_profile_from_target(
adapter, profile_name=profile_name
)
else:
# When dbt init is run outside of an existing project,
# create a new project and set up the user's profile.
project_name = click.prompt("What is the desired project name?")
project_path = Path(project_name)
if project_path.exists():
logger.info(
f"A project called {project_name} already exists here."
)
return
addendum = self.get_addendum(project_dir, profiles_dir)
logger.info(addendum)
self.copy_starter_repo(project_name)
os.chdir(project_name)
with open("dbt_project.yml", "r+") as f:
content = f"{f.read()}".format(
project_name=project_name,
profile_name=project_name
)
f.seek(0)
f.write(content)
f.truncate()
if not self.check_if_can_write_profile(profile_name=project_name):
return
adapter = self.ask_for_adapter_choice()
self.create_profile_from_target(
adapter, profile_name=project_name
)
logger.info(self.get_addendum(project_name, profiles_dir))

View File

@@ -11,8 +11,14 @@ from dbt.adapters.factory import get_adapter
from dbt.parser.manifest import (
Manifest, ManifestLoader, _check_manifest
)
from dbt.logger import DbtProcessState, print_timestamped_line
from dbt.logger import DbtProcessState
from dbt.clients.system import write_file
from dbt.events.types import (
ManifestDependenciesLoaded, ManifestLoaderCreated, ManifestLoaded, ManifestChecked,
ManifestFlatGraphBuilt, ParsingStart, ParsingCompiling, ParsingWritingManifest, ParsingDone,
ReportPerformancePath
)
from dbt.events.functions import fire_event
from dbt.graph import Graph
import time
from typing import Optional
@@ -40,7 +46,7 @@ class ParseTask(ConfiguredTask):
path = os.path.join(self.config.target_path, PERF_INFO_FILE_NAME)
write_file(path, json.dumps(self.loader._perf_info,
cls=dbt.utils.JSONEncoder, indent=4))
print_timestamped_line(f"Performance info: {path}")
fire_event(ReportPerformancePath(path=path))
# This method takes code that normally exists in other files
# and pulls it in here, to simplify logging and make the
@@ -58,22 +64,22 @@ class ParseTask(ConfiguredTask):
with PARSING_STATE:
start_load_all = time.perf_counter()
projects = root_config.load_dependencies()
print_timestamped_line("Dependencies loaded")
fire_event(ManifestDependenciesLoaded())
loader = ManifestLoader(root_config, projects, macro_hook)
print_timestamped_line("ManifestLoader created")
fire_event(ManifestLoaderCreated())
manifest = loader.load()
print_timestamped_line("Manifest loaded")
fire_event(ManifestLoaded())
_check_manifest(manifest, root_config)
print_timestamped_line("Manifest checked")
fire_event(ManifestChecked())
manifest.build_flat_graph()
print_timestamped_line("Flat graph built")
fire_event(ManifestFlatGraphBuilt())
loader._perf_info.load_all_elapsed = (
time.perf_counter() - start_load_all
)
self.loader = loader
self.manifest = manifest
print_timestamped_line("Manifest loaded")
fire_event(ManifestLoaded())
def compile_manifest(self):
adapter = get_adapter(self.config)
@@ -81,14 +87,14 @@ class ParseTask(ConfiguredTask):
self.graph = compiler.compile(self.manifest)
def run(self):
print_timestamped_line('Start parsing.')
fire_event(ParsingStart())
self.get_full_manifest()
if self.args.compile:
print_timestamped_line('Compiling.')
fire_event(ParsingCompiling())
self.compile_manifest()
if self.args.write_manifest:
print_timestamped_line('Writing manifest.')
fire_event(ParsingWritingManifest())
self.write_manifest()
self.write_perf_info()
print_timestamped_line('Done.')
fire_event(ParsingDone())

View File

@@ -1,3 +1,5 @@
from distutils.util import strtobool
from dataclasses import dataclass
from dbt import utils
from dbt.dataclass_schema import dbtClassMixin
@@ -19,6 +21,7 @@ from dbt.context.providers import generate_runtime_model
from dbt.clients.jinja import MacroGenerator
from dbt.exceptions import (
InternalException,
invalid_bool_error,
missing_materialization
)
from dbt.graph import (
@@ -34,6 +37,23 @@ class TestResultData(dbtClassMixin):
should_warn: bool
should_error: bool
@classmethod
def validate(cls, data):
data['should_warn'] = cls.convert_bool_type(data['should_warn'])
data['should_error'] = cls.convert_bool_type(data['should_error'])
super().validate(data)
def convert_bool_type(field) -> bool:
# if it's type string let python decide if it's a valid value to convert to bool
if isinstance(field, str):
try:
return bool(strtobool(field)) # type: ignore
except ValueError:
raise invalid_bool_error(field, 'get_test_sql')
# need this so we catch both true bools and 0/1
return bool(field)
class TestRunner(CompileRunner):
def describe_node(self):

View File

@@ -96,5 +96,5 @@ def _get_dbt_plugins_info():
yield plugin_name, mod.version
__version__ = '1.0.0b1'
__version__ = '1.0.0b2'
installed = get_installed_version()

View File

@@ -284,12 +284,12 @@ def parse_args(argv=None):
parser.add_argument('adapter')
parser.add_argument('--title-case', '-t', default=None)
parser.add_argument('--dependency', action='append')
parser.add_argument('--dbt-core-version', default='1.0.0b1')
parser.add_argument('--dbt-core-version', default='1.0.0b2')
parser.add_argument('--email')
parser.add_argument('--author')
parser.add_argument('--url')
parser.add_argument('--sql', action='store_true')
parser.add_argument('--package-version', default='1.0.0b1')
parser.add_argument('--package-version', default='1.0.0b2')
parser.add_argument('--project-version', default='1.0')
parser.add_argument(
'--no-dependency', action='store_false', dest='set_dependency'

View File

@@ -24,7 +24,7 @@ def read(fname):
package_name = "dbt-core"
package_version = "1.0.0b1"
package_version = "1.0.0b2"
description = """dbt (data build tool) is a command line tool that helps \
analysts and engineers transform data in their warehouse more effectively"""
@@ -50,8 +50,8 @@ setup(
],
install_requires=[
'Jinja2==2.11.3',
'PyYAML>=3.11',
'agate>=1.6,<1.6.2',
'click>=8,<9',
'colorama>=0.3.9,<0.4.5',
'dataclasses>=0.6,<0.9;python_version<"3.7"',
'hologram==0.0.14',
@@ -60,7 +60,7 @@ setup(
'mashumaro==2.5',
'minimal-snowplow-tracker==0.0.2',
'networkx>=2.3,<3',
'packaging~=20.9',
'packaging>=20.9,<22.0',
'sqlparse>=0.2.3,<0.5',
'dbt-extractor==0.4.0',
'typing-extensions>=3.7.4,<3.11',

View File

@@ -2,14 +2,15 @@ agate==1.6.1
attrs==21.2.0
Babel==2.9.1
certifi==2021.10.8
cffi==1.14.6
charset-normalizer==2.0.6
cffi==1.15.0
charset-normalizer==2.0.7
click==8.0.3
colorama==0.4.4
dbt-core==1.0.0a1
dbt-core==1.0.0b1
dbt-extractor==0.4.0
dbt-postgres==1.0.0a1
dbt-postgres==1.0.0b1
hologram==0.0.14
idna==3.2
idna==3.3
importlib-metadata==4.8.1
isodate==0.6.0
Jinja2==2.11.3
@@ -31,7 +32,7 @@ python-dateutil==2.8.2
python-slugify==5.0.2
pytimeparse==1.1.8
pytz==2021.3
PyYAML==5.4.1
PyYAML==6.0
requests==2.26.0
six==1.16.0
sqlparse==0.4.2

View File

@@ -1,3 +1,3 @@
[mypy]
mypy_path = ./third-party-stubs
namespace_packages = True
namespace_packages = True

View File

@@ -1 +1 @@
version = '1.0.0b1'
version = '1.0.0b2'

View File

@@ -0,0 +1,21 @@
fixed:
type: postgres
prompts:
host:
hint: 'hostname for the instance'
port:
default: 5432
type: 'int'
user:
hint: 'dev username'
pass:
hint: 'dev password'
hide_input: true
dbname:
hint: 'default database that dbt will build objects in'
schema:
hint: 'default schema that dbt will build objects in'
threads:
hint: '1 or more'
type: 'int'
default: 1

View File

@@ -41,7 +41,7 @@ def _dbt_psycopg2_name():
package_name = "dbt-postgres"
package_version = "1.0.0b1"
package_version = "1.0.0b2"
description = """The postgres adpter plugin for dbt (data build tool)"""
this_directory = os.path.abspath(os.path.dirname(__file__))

View File

@@ -24,7 +24,7 @@ with open(os.path.join(this_directory, 'README.md')) as f:
package_name = "dbt"
package_version = "1.0.0b1"
package_version = "1.0.0b2"
description = """With dbt, data analysts and engineers can build analytics \
the way engineers build applications."""
@@ -39,7 +39,7 @@ setup(
author="dbt Labs",
author_email="info@dbtlabs.com",
url="https://github.com/dbt-labs/dbt",
url="https://github.com/dbt-labs/dbt-core",
packages=[],
install_requires=[
'dbt-core=={}'.format(package_version),

View File

@@ -326,7 +326,7 @@ class TestGraphSelection(DBTIntegrationTest):
results = self.run_dbt(['ls', '--select', '1+exposure:user_exposure'])
assert len(results) == 5
assert sorted(results) == ['exposure:test.user_exposure', 'test.unique_users_id',
assert sorted(results) == ['exposure:test.user_exposure', 'test.unique_users_id',
'test.unique_users_rollup_gender', 'test.users', 'test.users_rollup']
results = self.run_dbt(['run', '-m', '+exposure:user_exposure'])

View File

@@ -0,0 +1,10 @@
{% macro get_test_sql(main_sql, fail_calc, warn_if, error_if, limit) -%}
select
{{ fail_calc }} as failures,
case when {{ fail_calc }} {{ warn_if }} then 1 else 0 end as should_warn,
case when {{ fail_calc }} {{ error_if }} then 1 else 0 end as should_error
from (
{{ main_sql }}
{{ "limit " ~ limit if limit != none }}
) dbt_internal_test
{%- endmacro %}

View File

@@ -0,0 +1,10 @@
{% macro get_test_sql(main_sql, fail_calc, warn_if, error_if, limit) -%}
select
{{ fail_calc }} as failures,
case when {{ fail_calc }} {{ warn_if }} then 'x' else 'y' end as should_warn,
case when {{ fail_calc }} {{ error_if }} then 'x' else 'y' end as should_error
from (
{{ main_sql }}
{{ "limit " ~ limit if limit != none }}
) dbt_internal_test
{% endmacro %}

View File

@@ -0,0 +1,3 @@
select * from {{ ref('my_model_pass') }}
UNION ALL
select null as id

View File

@@ -0,0 +1,3 @@
select 1 as id
UNION ALL
select null as id

View File

@@ -0,0 +1 @@
select * from {{ ref('my_model_pass') }}

View File

@@ -0,0 +1,31 @@
version: 2
models:
- name: my_model_pass
description: "The table has 1 null values, and we're okay with that, until it's more than 1."
columns:
- name: id
description: "The number of responses for this favorite color - purple will be null"
tests:
- not_null:
error_if: '>1'
warn_if: '>1'
- name: my_model_warning
description: "The table has 1 null values, and we're okay with that, but let us know"
columns:
- name: id
description: "The number of responses for this favorite color - purple will be null"
tests:
- not_null:
error_if: '>1'
- name: my_model_failure
description: "The table has 2 null values, and we're not okay with that"
columns:
- name: id
description: "The number of responses for this favorite color - purple will be null"
tests:
- not_null:
error_if: '>1'

View File

@@ -0,0 +1,3 @@
select 1 as id
UNION ALL
select null as id

View File

@@ -0,0 +1,12 @@
version: 2
models:
- name: my_model
description: "The table has 1 null values, and we're not okay with that."
columns:
- name: id
description: "The number of responses for this favorite color - purple will be null"
tests:
- not_null

View File

@@ -159,6 +159,178 @@ class TestLimitedSchemaTests(DBTIntegrationTest):
self.assertEqual(sum(x.failures for x in test_results), 3)
class TestDefaultBoolType(DBTIntegrationTest):
# test with default True/False in get_test_sql macro
def setUp(self):
DBTIntegrationTest.setUp(self)
@property
def schema(self):
return "schema_tests_008"
@property
def models(self):
return "models-v2/override_get_test_models"
def run_schema_validations(self):
args = FakeArgs()
test_task = TestTask(args, self.config)
return test_task.run()
def assertTestFailed(self, result):
self.assertEqual(result.status, "fail")
self.assertFalse(result.skipped)
self.assertTrue(
result.failures > 0,
'test {} did not fail'.format(result.node.name)
)
def assertTestWarn(self, result):
self.assertEqual(result.status, "warn")
self.assertFalse(result.skipped)
self.assertTrue(
result.failures > 0,
'test {} passed without expected warning'.format(result.node.name)
)
def assertTestPassed(self, result):
self.assertEqual(result.status, "pass")
self.assertFalse(result.skipped)
self.assertEqual(
result.failures, 0,
'test {} failed'.format(result.node.name)
)
@use_profile('postgres')
def test_postgres_limit_schema_tests(self):
results = self.run_dbt()
self.assertEqual(len(results), 3)
test_results = self.run_schema_validations()
self.assertEqual(len(test_results), 3)
for result in test_results:
# assert that all deliberately failing tests actually fail
if 'failure' in result.node.name:
self.assertTestFailed(result)
# assert that tests with warnings have them
elif 'warning' in result.node.name:
self.assertTestWarn(result)
# assert that actual tests pass
else:
self.assertTestPassed(result)
# warnings are also marked as failures
self.assertEqual(sum(x.failures for x in test_results), 3)
class TestOtherBoolType(DBTIntegrationTest):
# test with expected 0/1 in custom get_test_sql macro
def setUp(self):
DBTIntegrationTest.setUp(self)
@property
def schema(self):
return "schema_tests_008"
@property
def models(self):
return "models-v2/override_get_test_models"
@property
def project_config(self):
return {
'config-version': 2,
"macro-paths": ["macros-v2/override_get_test_macros"],
}
def run_schema_validations(self):
args = FakeArgs()
test_task = TestTask(args, self.config)
return test_task.run()
def assertTestFailed(self, result):
self.assertEqual(result.status, "fail")
self.assertFalse(result.skipped)
self.assertTrue(
result.failures > 0,
'test {} did not fail'.format(result.node.name)
)
def assertTestWarn(self, result):
self.assertEqual(result.status, "warn")
self.assertFalse(result.skipped)
self.assertTrue(
result.failures > 0,
'test {} passed without expected warning'.format(result.node.name)
)
def assertTestPassed(self, result):
self.assertEqual(result.status, "pass")
self.assertFalse(result.skipped)
self.assertEqual(
result.failures, 0,
'test {} failed'.format(result.node.name)
)
@use_profile('postgres')
def test_postgres_limit_schema_tests(self):
results = self.run_dbt()
self.assertEqual(len(results), 3)
test_results = self.run_schema_validations()
self.assertEqual(len(test_results), 3)
for result in test_results:
# assert that all deliberately failing tests actually fail
if 'failure' in result.node.name:
self.assertTestFailed(result)
# assert that tests with warnings have them
elif 'warning' in result.node.name:
self.assertTestWarn(result)
# assert that actual tests pass
else:
self.assertTestPassed(result)
# warnings are also marked as failures
self.assertEqual(sum(x.failures for x in test_results), 3)
class TestNonBoolType(DBTIntegrationTest):
# test with invalid 'x'/'y' in custom get_test_sql macro
def setUp(self):
DBTIntegrationTest.setUp(self)
@property
def schema(self):
return "schema_tests_008"
@property
def models(self):
return "models-v2/override_get_test_models_fail"
@property
def project_config(self):
return {
'config-version': 2,
"macro-paths": ["macros-v2/override_get_test_macros_fail"],
}
def run_schema_validations(self):
args = FakeArgs()
test_task = TestTask(args, self.config)
return test_task.run()
@use_profile('postgres')
def test_postgres_limit_schema_tests(self):
results = self.run_dbt()
self.assertEqual(len(results), 1)
run_result = self.run_dbt(['test'], expect_pass=False)
results = run_result.results
self.assertEqual(len(results), 1)
self.assertEqual(results[0].status, TestStatus.Error)
self.assertRegex(results[0].message, r"'get_test_sql' returns 'x'")
class TestMalformedSchemaTests(DBTIntegrationTest):
def setUp(self):
@@ -544,7 +716,6 @@ class TestSchemaTestContextWithMacroNamespace(DBTIntegrationTest):
run_result = self.run_dbt(['test'], expect_pass=False)
results = run_result.results
results = sorted(results, key=lambda r: r.node.name)
# breakpoint()
self.assertEqual(len(results), 4)
# call_pkg_macro_model_c_
self.assertEqual(results[0].status, TestStatus.Fail)
@@ -622,4 +793,3 @@ class TestWrongSpecificationBlock(DBTIntegrationTest):
assert len(results) == 1
assert results[0] == '{"name": "some_seed", "description": ""}'

View File

@@ -0,0 +1,3 @@
{% macro postgres__get_columns_in_relation(relation) %}
{{ return('a string') }}
{% endmacro %}

View File

@@ -118,6 +118,64 @@ class TestMacroOverrideBuiltin(DBTIntegrationTest):
self.run_dbt()
class TestMacroOverridePackage(DBTIntegrationTest):
"""
The macro in `override-postgres-get-columns-macros` should override the
`get_columns_in_relation` macro by default.
"""
@property
def schema(self):
return "test_macros_016"
@property
def models(self):
return 'override-get-columns-models'
@property
def project_config(self):
return {
'config-version': 2,
'macro-paths': ['override-postgres-get-columns-macros'],
}
@use_profile('postgres')
def test_postgres_overrides(self):
# the first time, the model doesn't exist
self.run_dbt()
self.run_dbt()
class TestMacroNotOverridePackage(DBTIntegrationTest):
"""
The macro in `override-postgres-get-columns-macros` does NOT override the
`get_columns_in_relation` macro because we tell dispatch to not look at the
postgres macros.
"""
@property
def schema(self):
return "test_macros_016"
@property
def models(self):
return 'override-get-columns-models'
@property
def project_config(self):
return {
'config-version': 2,
'macro-paths': ['override-postgres-get-columns-macros'],
'dispatch': [{'macro_namespace': 'dbt', 'search_order': ['dbt']}],
}
@use_profile('postgres')
def test_postgres_overrides(self):
# the first time, the model doesn't exist
self.run_dbt(expect_pass=False)
self.run_dbt(expect_pass=False)
class TestDispatchMacroOverrideBuiltin(TestMacroOverrideBuiltin):
# test the same functionality as above, but this time,
# dbt.get_columns_in_relation will dispatch to a default__ macro

View File

@@ -1,8 +1,12 @@
from test.integration.base import DBTIntegrationTest, use_profile
import os
import shutil
import yaml
from unittest import mock
from unittest.mock import Mock, call
from pathlib import Path
import click
from test.integration.base import DBTIntegrationTest, use_profile
class TestInit(DBTIntegrationTest):
@@ -15,29 +19,400 @@ class TestInit(DBTIntegrationTest):
super().tearDown()
def get_project_name(self):
return "my_project_{}".format(self.unique_schema())
return 'my_project_{}'.format(self.unique_schema())
@property
def schema(self):
return "init_040"
return 'init_040'
@property
def models(self):
return "models"
return 'models'
@use_profile('postgres')
def test_postgres_init_task(self):
@mock.patch('click.confirm')
@mock.patch('click.prompt')
def test_postgres_init_task_in_project_with_existing_profiles_yml(self, mock_prompt, mock_confirm):
manager = Mock()
manager.attach_mock(mock_prompt, 'prompt')
manager.attach_mock(mock_confirm, 'confirm')
manager.confirm.side_effect = ["y"]
manager.prompt.side_effect = [
1,
'localhost',
5432,
'test_user',
'test_password',
'test_db',
'test_schema',
4,
]
self.run_dbt(['init'])
manager.assert_has_calls([
call.confirm(f"The profile test already exists in {os.path.join(self.test_root_dir, 'profiles.yml')}. Continue and overwrite it?"),
call.prompt("Which database would you like to use?\n[1] postgres\n\n(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)\n\nEnter a number", type=click.INT),
call.prompt('host (hostname for the instance)', default=None, hide_input=False, type=None),
call.prompt('port', default=5432, hide_input=False, type=click.INT),
call.prompt('user (dev username)', default=None, hide_input=False, type=None),
call.prompt('pass (dev password)', default=None, hide_input=True, type=None),
call.prompt('dbname (default database that dbt will build objects in)', default=None, hide_input=False, type=None),
call.prompt('schema (default schema that dbt will build objects in)', default=None, hide_input=False, type=None),
call.prompt('threads (1 or more)', default=1, hide_input=False, type=click.INT),
])
with open(os.path.join(self.test_root_dir, 'profiles.yml'), 'r') as f:
assert f.read() == """config:
send_anonymous_usage_stats: false
test:
outputs:
dev:
dbname: test_db
host: localhost
pass: test_password
port: 5432
schema: test_schema
threads: 4
type: postgres
user: test_user
target: dev
"""
@use_profile('postgres')
@mock.patch('click.confirm')
@mock.patch('click.prompt')
@mock.patch.object(Path, 'exists', autospec=True)
def test_postgres_init_task_in_project_without_existing_profiles_yml(self, exists, mock_prompt, mock_confirm):
def exists_side_effect(path):
# Override responses on specific files, default to 'real world' if not overriden
return {
'profiles.yml': False
}.get(path.name, os.path.exists(path))
exists.side_effect = exists_side_effect
manager = Mock()
manager.attach_mock(mock_prompt, 'prompt')
manager.prompt.side_effect = [
1,
'localhost',
5432,
'test_user',
'test_password',
'test_db',
'test_schema',
4,
]
self.run_dbt(['init'])
manager.assert_has_calls([
call.prompt("Which database would you like to use?\n[1] postgres\n\n(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)\n\nEnter a number", type=click.INT),
call.prompt('host (hostname for the instance)', default=None, hide_input=False, type=None),
call.prompt('port', default=5432, hide_input=False, type=click.INT),
call.prompt('user (dev username)', default=None, hide_input=False, type=None),
call.prompt('pass (dev password)', default=None, hide_input=True, type=None),
call.prompt('dbname (default database that dbt will build objects in)', default=None, hide_input=False, type=None),
call.prompt('schema (default schema that dbt will build objects in)', default=None, hide_input=False, type=None),
call.prompt('threads (1 or more)', default=1, hide_input=False, type=click.INT)
])
with open(os.path.join(self.test_root_dir, 'profiles.yml'), 'r') as f:
assert f.read() == """test:
outputs:
dev:
dbname: test_db
host: localhost
pass: test_password
port: 5432
schema: test_schema
threads: 4
type: postgres
user: test_user
target: dev
"""
@use_profile('postgres')
@mock.patch('click.confirm')
@mock.patch('click.prompt')
@mock.patch.object(Path, 'exists', autospec=True)
def test_postgres_init_task_in_project_without_existing_profiles_yml_or_profile_template(self, exists, mock_prompt, mock_confirm):
def exists_side_effect(path):
# Override responses on specific files, default to 'real world' if not overriden
return {
'profiles.yml': False,
'profile_template.yml': False,
}.get(path.name, os.path.exists(path))
exists.side_effect = exists_side_effect
manager = Mock()
manager.attach_mock(mock_prompt, 'prompt')
manager.attach_mock(mock_confirm, 'confirm')
manager.prompt.side_effect = [
1,
]
self.run_dbt(['init'])
manager.assert_has_calls([
call.prompt("Which database would you like to use?\n[1] postgres\n\n(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)\n\nEnter a number", type=click.INT),
])
with open(os.path.join(self.test_root_dir, 'profiles.yml'), 'r') as f:
assert f.read() == """test:
outputs:
dev:
type: postgres
threads: [1 or more]
host: [host]
port: [port]
user: [dev_username]
pass: [dev_password]
dbname: [dbname]
schema: [dev_schema]
prod:
type: postgres
threads: [1 or more]
host: [host]
port: [port]
user: [prod_username]
pass: [prod_password]
dbname: [dbname]
schema: [prod_schema]
target: dev
"""
@use_profile('postgres')
@mock.patch('click.confirm')
@mock.patch('click.prompt')
@mock.patch.object(Path, 'exists', autospec=True)
def test_postgres_init_task_in_project_with_profile_template_without_existing_profiles_yml(self, exists, mock_prompt, mock_confirm):
def exists_side_effect(path):
# Override responses on specific files, default to 'real world' if not overriden
return {
'profiles.yml': False,
}.get(path.name, os.path.exists(path))
exists.side_effect = exists_side_effect
with open("profile_template.yml", 'w') as f:
f.write("""fixed:
type: postgres
threads: 4
host: localhost
dbname: my_db
schema: my_schema
prompts:
port:
hint: 'The port (for integer test purposes)'
type: int
default: 5432
user:
hint: 'Your username'
pass:
hint: 'Your password'
hide_input: true""")
manager = Mock()
manager.attach_mock(mock_prompt, 'prompt')
manager.attach_mock(mock_confirm, 'confirm')
manager.prompt.side_effect = [
5432,
'test_username',
'test_password'
]
self.run_dbt(['init'])
manager.assert_has_calls([
call.prompt('port (The port (for integer test purposes))', default=5432, hide_input=False, type=click.INT),
call.prompt('user (Your username)', default=None, hide_input=False, type=None),
call.prompt('pass (Your password)', default=None, hide_input=True, type=None)
])
with open(os.path.join(self.test_root_dir, 'profiles.yml'), 'r') as f:
assert f.read() == """test:
outputs:
dev:
dbname: my_db
host: localhost
pass: test_password
port: 5432
schema: my_schema
threads: 4
type: postgres
user: test_username
target: dev
"""
@use_profile('postgres')
@mock.patch('click.confirm')
@mock.patch('click.prompt')
def test_postgres_init_task_in_project_with_invalid_profile_template(self, mock_prompt, mock_confirm):
"""Test that when an invalid profile_template.yml is provided in the project,
init command falls back to the target's profile_template.yml"""
with open("profile_template.yml", 'w') as f:
f.write("""invalid template""")
manager = Mock()
manager.attach_mock(mock_prompt, 'prompt')
manager.attach_mock(mock_confirm, 'confirm')
manager.confirm.side_effect = ["y"]
manager.prompt.side_effect = [
1,
'localhost',
5432,
'test_username',
'test_password',
'test_db',
'test_schema',
4,
]
self.run_dbt(['init'])
manager.assert_has_calls([
call.confirm(f"The profile test already exists in {os.path.join(self.test_root_dir, 'profiles.yml')}. Continue and overwrite it?"),
call.prompt("Which database would you like to use?\n[1] postgres\n\n(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)\n\nEnter a number", type=click.INT),
call.prompt('host (hostname for the instance)', default=None, hide_input=False, type=None),
call.prompt('port', default=5432, hide_input=False, type=click.INT),
call.prompt('user (dev username)', default=None, hide_input=False, type=None),
call.prompt('pass (dev password)', default=None, hide_input=True, type=None),
call.prompt('dbname (default database that dbt will build objects in)', default=None, hide_input=False, type=None),
call.prompt('schema (default schema that dbt will build objects in)', default=None, hide_input=False, type=None),
call.prompt('threads (1 or more)', default=1, hide_input=False, type=click.INT)
])
with open(os.path.join(self.test_root_dir, 'profiles.yml'), 'r') as f:
assert f.read() == """config:
send_anonymous_usage_stats: false
test:
outputs:
dev:
dbname: test_db
host: localhost
pass: test_password
port: 5432
schema: test_schema
threads: 4
type: postgres
user: test_username
target: dev
"""
@use_profile('postgres')
@mock.patch('click.confirm')
@mock.patch('click.prompt')
def test_postgres_init_task_outside_of_project(self, mock_prompt, mock_confirm):
manager = Mock()
manager.attach_mock(mock_prompt, 'prompt')
manager.attach_mock(mock_confirm, 'confirm')
# Start by removing the dbt_project.yml so that we're not in an existing project
os.remove('dbt_project.yml')
project_name = self.get_project_name()
self.run_dbt(['init', project_name, '--adapter', 'postgres'])
manager.prompt.side_effect = [
project_name,
1,
'localhost',
5432,
'test_username',
'test_password',
'test_db',
'test_schema',
4,
]
self.run_dbt(['init'])
manager.assert_has_calls([
call.prompt('What is the desired project name?'),
call.prompt("Which database would you like to use?\n[1] postgres\n\n(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)\n\nEnter a number", type=click.INT),
call.prompt('host (hostname for the instance)', default=None, hide_input=False, type=None),
call.prompt('port', default=5432, hide_input=False, type=click.INT),
call.prompt('user (dev username)', default=None, hide_input=False, type=None),
call.prompt('pass (dev password)', default=None, hide_input=True, type=None),
call.prompt('dbname (default database that dbt will build objects in)', default=None, hide_input=False, type=None),
call.prompt('schema (default schema that dbt will build objects in)', default=None, hide_input=False, type=None),
call.prompt('threads (1 or more)', default=1, hide_input=False, type=click.INT),
])
assert os.path.exists(project_name)
project_file = os.path.join(project_name, 'dbt_project.yml')
assert os.path.exists(project_file)
with open(project_file) as fp:
project_data = yaml.safe_load(fp.read())
with open(os.path.join(self.test_root_dir, 'profiles.yml'), 'r') as f:
assert f.read() == f"""config:
send_anonymous_usage_stats: false
{project_name}:
outputs:
dev:
dbname: test_db
host: localhost
pass: test_password
port: 5432
schema: test_schema
threads: 4
type: postgres
user: test_username
target: dev
test:
outputs:
default2:
dbname: dbt
host: localhost
pass: password
port: 5432
schema: {self.unique_schema()}
threads: 4
type: postgres
user: root
noaccess:
dbname: dbt
host: localhost
pass: password
port: 5432
schema: {self.unique_schema()}
threads: 4
type: postgres
user: noaccess
target: default2
"""
assert 'config-version' in project_data
assert project_data['config-version'] == 2
with open(os.path.join(self.test_root_dir, project_name, 'dbt_project.yml'), 'r') as f:
assert f.read() == f"""
# 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: '{project_name}'
version: '1.0.0'
config-version: 2
git_dir = os.path.join(project_name, '.git')
assert not os.path.exists(git_dir)
# This setting configures which "profile" dbt uses for this project.
profile: '{project_name}'
# These configurations specify where dbt should look for different types of files.
# The `model-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: ["analyses"]
test-paths: ["tests"]
seed-paths: ["seeds"]
macro-paths: ["macros"]
snapshot-paths: ["snapshots"]
target-path: "target" # directory which will store compiled SQL files
clean-targets: # directories to be removed by `dbt clean`
- "target"
- "dbt_packages"
# Configuring models
# Full documentation: https://docs.getdbt.com/docs/configuring-models
# In this example config, we tell dbt to build all models in the example/ directory
# as tables. These settings can be overridden in the individual model files
# using the `{{{{ config(...) }}}}` macro.
models:
{project_name}:
# Config indicated by + and applies to all files under models/example/
example:
+materialized: view
"""

View File

@@ -372,7 +372,7 @@ class TestSourceFreshness(SuccessfulSourcesTest):
@use_profile('postgres')
def test_postgres_source_freshness_selection_exclude(self):
"""Tests node selection using the --select argument. It 'excludes' the
"""Tests node selection using the --select argument. It 'excludes' the
only source in the project so it should return no results."""
self._set_updated_at_to(timedelta(hours=-2))
self.freshness_start_time = datetime.utcnow()

View File

@@ -0,0 +1,9 @@
version: 2
models:
- name: orders
description: "Some order data"
columns:
- name: id
tests:
- unique

View File

@@ -0,0 +1,26 @@
{% test is_odd(model, column_name) %}
with validation as (
select
{{ column_name }} as odd_field
from {{ model }}
),
validation_errors as (
select
odd_field
from validation
-- if this is true, then odd_field is actually even!
where (odd_field % 2) = 0
)
select *
from validation_errors
{% endtest %}

View File

@@ -0,0 +1,26 @@
{% test is_odd(model, column_name) %}
with validation as (
select
{{ column_name }} as odd_field2
from {{ model }}
),
validation_errors as (
select
odd_field2
from validation
-- if this is true, then odd_field is actually even!
where (odd_field2 % 2) = 0
)
select *
from validation_errors
{% endtest %}

View File

@@ -0,0 +1,10 @@
version: 2
models:
- name: orders
description: "Some order data"
columns:
- name: id
tests:
- unique
- is_odd

View File

@@ -41,6 +41,7 @@ class BasePPTest(DBTIntegrationTest):
# delete files in this directory without tests interfering with each other.
os.mkdir(os.path.join(self.test_root_dir, 'models'))
os.mkdir(os.path.join(self.test_root_dir, 'tests'))
os.mkdir(os.path.join(self.test_root_dir, 'tests/generic'))
os.mkdir(os.path.join(self.test_root_dir, 'seeds'))
os.mkdir(os.path.join(self.test_root_dir, 'macros'))
os.mkdir(os.path.join(self.test_root_dir, 'analyses'))
@@ -476,6 +477,7 @@ class TestMacros(BasePPTest):
results, log_output = self.run_dbt_and_capture(['--partial-parse', 'run'])
self.assertTrue('Starting full parse.' in log_output)
class TestSnapshots(BasePPTest):
@use_profile('postgres')
@@ -508,3 +510,40 @@ class TestSnapshots(BasePPTest):
self.rm_file(normalize('snapshots/snapshot.sql'))
results = self.run_dbt(["--partial-parse", "run"])
self.assertEqual(len(results), 1)
class TestTests(BasePPTest):
@use_profile('postgres')
def test_postgres_pp_generic_tests(self):
# initial run
self.setup_directories()
self.copy_file('test-files/orders.sql', 'models/orders.sql')
self.copy_file('test-files/generic_schema.yml', 'models/schema.yml')
results = self.run_dbt()
self.assertEqual(len(results), 1)
manifest = get_manifest()
expected_nodes = ['model.test.orders', 'test.test.unique_orders_id.1360ecc70e']
self.assertCountEqual(expected_nodes, list(manifest.nodes.keys()))
# add generic test in test-path
self.copy_file('test-files/generic_test.sql', 'tests/generic/generic_test.sql')
self.copy_file('test-files/generic_test_schema.yml', 'models/schema.yml')
results = self.run_dbt(["--partial-parse", "run"])
self.assertEqual(len(results), 1)
manifest = get_manifest()
test_id = 'test.test.is_odd_orders_id.82834fdc5b'
self.assertIn(test_id, manifest.nodes)
expected_nodes = ['model.test.orders', 'test.test.unique_orders_id.1360ecc70e', 'test.test.is_odd_orders_id.82834fdc5b']
self.assertCountEqual(expected_nodes, list(manifest.nodes.keys()))
# edit generic test in test-path
self.copy_file('test-files/generic_test_edited.sql', 'tests/generic/generic_test.sql')
results = self.run_dbt(["--partial-parse", "run"])
self.assertEqual(len(results), 1)
manifest = get_manifest()
test_id = 'test.test.is_odd_orders_id.82834fdc5b'
self.assertIn(test_id, manifest.nodes)
expected_nodes = ['model.test.orders', 'test.test.unique_orders_id.1360ecc70e', 'test.test.is_odd_orders_id.82834fdc5b']
self.assertCountEqual(expected_nodes, list(manifest.nodes.keys()))

10
test/setup_db.sh Normal file → Executable file
View File

@@ -1,6 +1,5 @@
#!/bin/bash
set -x
env | grep '^PG'
# If you want to run this script for your own postgresql (run with
@@ -30,6 +29,15 @@ if [[ -n $CIRCLECI ]]; then
connect_circle
fi
for i in {1..10}; do
if pg_isready -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" ; then
break
fi
echo "Waiting for postgres to be ready..."
sleep 2;
done;
createdb dbt
psql -c "CREATE ROLE root WITH PASSWORD 'password';"
psql -c "ALTER ROLE root WITH LOGIN;"

View File

@@ -71,7 +71,7 @@ class SelectorUnitTest(unittest.TestCase):
definition = sel_dict['nightly_selector']['definition']
self.assertEqual(expected, definition)
def test_single_key_value_definition(self):
dct = get_selector_dict('''\
selectors:

View File

@@ -5,13 +5,15 @@ from unittest import mock
import os
import yaml
from copy import deepcopy
import dbt.flags
import dbt.parser
from dbt import tracking
from dbt.context.context_config import ContextConfig
from dbt.exceptions import CompilationException
from dbt.parser import (
ModelParser, MacroParser, SingularTestParser, SchemaParser,
SnapshotParser, AnalysisParser
ModelParser, MacroParser, SingularTestParser, GenericTestParser,
SchemaParser, SnapshotParser, AnalysisParser
)
from dbt.parser.schemas import (
TestablePatchParser, SourceParser, AnalysisPatchParser, MacroPatchParser
@@ -28,10 +30,13 @@ from dbt.contracts.graph.model_config import (
)
from dbt.contracts.graph.parsed import (
ParsedModelNode, ParsedMacro, ParsedNodePatch, DependsOn, ColumnInfo,
ParsedSingularTestNode, ParsedSnapshotNode, ParsedAnalysisNode,
UnpatchedSourceDefinition
ParsedSingularTestNode, ParsedGenericTestNode, ParsedSnapshotNode,
ParsedAnalysisNode, UnpatchedSourceDefinition
)
from dbt.contracts.graph.unparsed import Docs
from dbt.parser.models import (
_get_config_call_dict, _shift_sources, _get_exp_sample_result, _get_stable_sample_result, _get_sample_result
)
import itertools
from .utils import config_from_parts_or_dicts, normalize, generate_name_macros, MockNode, MockSource, MockDocumentation
@@ -571,6 +576,167 @@ class StaticModelParserTest(BaseParserTest):
assert(self.parser._has_banned_macro(node))
# TODO
class StaticModelParserUnitTest(BaseParserTest):
# _get_config_call_dict
# _shift_sources
# _get_exp_sample_result
# _get_stable_sample_result
# _get_sample_result
def setUp(self):
super().setUp()
self.parser = ModelParser(
project=self.snowplow_project_config,
manifest=self.manifest,
root_project=self.root_project_config,
)
self.example_node = ParsedModelNode(
alias='model_1',
name='model_1',
database='test',
schema='analytics',
resource_type=NodeType.Model,
unique_id='model.snowplow.model_1',
fqn=['snowplow', 'nested', 'model_1'],
package_name='snowplow',
original_file_path=normalize('models/nested/model_1.sql'),
root_path=get_abs_os_path('./dbt_packages/snowplow'),
config=NodeConfig(materialized='table'),
path=normalize('nested/model_1.sql'),
raw_sql='{{ config(materialized="table") }}select 1 as id',
checksum=None,
unrendered_config={'materialized': 'table'},
)
self.example_config = ContextConfig(
self.root_project_config,
self.example_node.fqn,
self.example_node.resource_type,
self.snowplow_project_config,
)
def file_block_for(self, data, filename):
return super().file_block_for(data, filename, 'models')
# tests that configs get extracted properly. the function should respect merge behavior,
# but becuase it's only reading from one dictionary it won't matter except in edge cases
# like this example with tags changing type to a list.
def test_config_shifting(self):
static_parser_result = {
'configs': [
('hello', 'world'),
('flag', True),
('tags', 'tag1'),
('tags', 'tag2')
]
}
expected = {
'hello': 'world',
'flag': True,
'tags': ['tag1', 'tag2']
}
got = _get_config_call_dict(static_parser_result)
self.assertEqual(expected, got)
def test_source_shifting(self):
static_parser_result = {
'sources': [('abc', 'def'), ('x', 'y')]
}
expected = {
'sources': [['abc', 'def'], ['x', 'y']]
}
got = _shift_sources(static_parser_result)
self.assertEqual(expected, got)
def test_sample_results(self):
# --- missed ref --- #
node = deepcopy(self.example_node)
config = deepcopy(self.example_config)
sample_node = deepcopy(self.example_node)
sample_config = deepcopy(self.example_config)
sample_node.refs = []
node.refs = ['myref']
result = _get_sample_result(sample_node, sample_config, node, config)
self.assertEqual([(7, "missed_ref_value")], result)
# --- false positive ref --- #
node = deepcopy(self.example_node)
config = deepcopy(self.example_config)
sample_node = deepcopy(self.example_node)
sample_config = deepcopy(self.example_config)
sample_node.refs = ['myref']
node.refs = []
result = _get_sample_result(sample_node, sample_config, node, config)
self.assertEqual([(6, "false_positive_ref_value")], result)
# --- missed source --- #
node = deepcopy(self.example_node)
config = deepcopy(self.example_config)
sample_node = deepcopy(self.example_node)
sample_config = deepcopy(self.example_config)
sample_node.sources = []
node.sources = [['abc', 'def']]
result = _get_sample_result(sample_node, sample_config, node, config)
self.assertEqual([(5, 'missed_source_value')], result)
# --- false positive source --- #
node = deepcopy(self.example_node)
config = deepcopy(self.example_config)
sample_node = deepcopy(self.example_node)
sample_config = deepcopy(self.example_config)
sample_node.sources = [['abc', 'def']]
node.sources = []
result = _get_sample_result(sample_node, sample_config, node, config)
self.assertEqual([(4, 'false_positive_source_value')], result)
# --- missed config --- #
node = deepcopy(self.example_node)
config = deepcopy(self.example_config)
sample_node = deepcopy(self.example_node)
sample_config = deepcopy(self.example_config)
sample_config._config_call_dict = {}
config._config_call_dict = {'key': 'value'}
result = _get_sample_result(sample_node, sample_config, node, config)
self.assertEqual([(3, 'missed_config_value')], result)
# --- false positive config --- #
node = deepcopy(self.example_node)
config = deepcopy(self.example_config)
sample_node = deepcopy(self.example_node)
sample_config = deepcopy(self.example_config)
sample_config._config_call_dict = {'key': 'value'}
config._config_call_dict = {}
result = _get_sample_result(sample_node, sample_config, node, config)
self.assertEqual([(2, "false_positive_config_value")], result)
def test_exp_sample_results(self):
node = deepcopy(self.example_node)
config = deepcopy(self.example_config)
sample_node = deepcopy(self.example_node)
sample_config = deepcopy(self.example_config)
result = _get_exp_sample_result(sample_node, sample_config, node, config)
self.assertEqual(["00_experimental_exact_match"], result)
def test_stable_sample_results(self):
node = deepcopy(self.example_node)
config = deepcopy(self.example_config)
sample_node = deepcopy(self.example_node)
sample_config = deepcopy(self.example_config)
result = _get_stable_sample_result(sample_node, sample_config, node, config)
self.assertEqual(["80_stable_exact_match"], result)
class SnapshotParserTest(BaseParserTest):
def setUp(self):
@@ -849,6 +1015,40 @@ class SingularTestParserTest(BaseParserTest):
self.assertEqual(self.parser.manifest.files[file_id].nodes, ['test.snowplow.test_1'])
class GenericTestParserTest(BaseParserTest):
# generic tests in the test-paths directory currently leverage the macro parser
def setUp(self):
super().setUp()
self.parser = GenericTestParser(
project=self.snowplow_project_config,
manifest=Manifest()
)
def file_block_for(self, data, filename):
return super().file_block_for(data, filename, 'tests/generic')
def test_basic(self):
raw_sql = '{% test not_null(model, column_name) %}select * from {{ model }} where {{ column_name }} is null {% endtest %}'
block = self.file_block_for(raw_sql, 'test_1.sql')
self.parser.manifest.files[block.file.file_id] = block.file
self.parser.parse_file(block)
node = list(self.parser.manifest.macros.values())[0]
expected = ParsedMacro(
name='test_not_null',
resource_type=NodeType.Macro,
unique_id='macro.snowplow.test_not_null',
package_name='snowplow',
original_file_path=normalize('tests/generic/test_1.sql'),
root_path=get_abs_os_path('./dbt_packages/snowplow'),
path=normalize('tests/generic/test_1.sql'),
macro_sql=raw_sql,
)
assertEqualNodes(node, expected)
file_id = 'snowplow://' + normalize('tests/generic/test_1.sql')
self.assertIn(file_id, self.parser.manifest.files)
self.assertEqual(self.parser.manifest.files[file_id].macros, ['macro.snowplow.test_not_null'])
class AnalysisParserTest(BaseParserTest):
def setUp(self):
super().setUp()

View File

@@ -35,7 +35,7 @@ description = adapter plugin integration testing
skip_install = true
passenv = DBT_* POSTGRES_TEST_* PYTEST_ADDOPTS
commands =
postgres: {envpython} -m pytest {posargs} -m profile_postgres test/integration
postgres: {envpython} -m pytest -m profile_postgres {posargs:test/integration}
deps =
-rdev-requirements.txt
-e./core