mirror of
https://github.com/dbt-labs/dbt-core
synced 2025-12-17 19:31:34 +00:00
Compare commits
24 Commits
feature/40
...
add-except
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d342165f6d | ||
|
|
a3dc5efda7 | ||
|
|
1015b89dbf | ||
|
|
5c9fd07050 | ||
|
|
c019a94206 | ||
|
|
f9bdfa050b | ||
|
|
1b35d1aa21 | ||
|
|
420ef9cc7b | ||
|
|
02fdc2cb9f | ||
|
|
f82745fb0c | ||
|
|
3397bdc6a5 | ||
|
|
96e858ac0b | ||
|
|
f6a98b5674 | ||
|
|
824f0bf2c0 | ||
|
|
5648b1c622 | ||
|
|
bb1382e576 | ||
|
|
085ea9181f | ||
|
|
eace5b77a7 | ||
|
|
1c61bb18e6 | ||
|
|
f79a968a09 | ||
|
|
34c23fe650 | ||
|
|
3ae9475655 | ||
|
|
11436fed45 | ||
|
|
21a7b71657 |
@@ -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+)
|
||||
|
||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
```
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
9
core/dbt/events/README.md
Normal file
9
core/dbt/events/README.md
Normal 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.
|
||||
0
core/dbt/events/__init__.py
Normal file
0
core/dbt/events/__init__.py
Normal file
30
core/dbt/events/functions.py
Normal file
30
core/dbt/events/functions.py
Normal 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()}"
|
||||
)
|
||||
7
core/dbt/events/history.py
Normal file
7
core/dbt/events/history.py
Normal 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
147
core/dbt/events/types.py
Normal 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='')
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
106
core/dbt/parser/generic_test.py
Normal file
106
core/dbt/parser/generic_test.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
2
mypy.ini
2
mypy.ini
@@ -1,3 +1,3 @@
|
||||
[mypy]
|
||||
mypy_path = ./third-party-stubs
|
||||
namespace_packages = True
|
||||
namespace_packages = True
|
||||
@@ -1 +1 @@
|
||||
version = '1.0.0b1'
|
||||
version = '1.0.0b2'
|
||||
|
||||
21
plugins/postgres/dbt/include/postgres/profile_template.yml
Normal file
21
plugins/postgres/dbt/include/postgres/profile_template.yml
Normal 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
|
||||
@@ -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__))
|
||||
|
||||
4
setup.py
4
setup.py
@@ -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),
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,3 @@
|
||||
select * from {{ ref('my_model_pass') }}
|
||||
UNION ALL
|
||||
select null as id
|
||||
@@ -0,0 +1,3 @@
|
||||
select 1 as id
|
||||
UNION ALL
|
||||
select null as id
|
||||
@@ -0,0 +1 @@
|
||||
select * from {{ ref('my_model_pass') }}
|
||||
@@ -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'
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
select 1 as id
|
||||
UNION ALL
|
||||
select null as id
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": ""}'
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{% macro postgres__get_columns_in_relation(relation) %}
|
||||
{{ return('a string') }}
|
||||
{% endmacro %}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
|
||||
models:
|
||||
- name: orders
|
||||
description: "Some order data"
|
||||
columns:
|
||||
- name: id
|
||||
tests:
|
||||
- unique
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
|
||||
models:
|
||||
- name: orders
|
||||
description: "Some order data"
|
||||
columns:
|
||||
- name: id
|
||||
tests:
|
||||
- unique
|
||||
- is_odd
|
||||
@@ -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
10
test/setup_db.sh
Normal file → Executable 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;"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
2
tox.ini
2
tox.ini
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user