forked from repo-mirrors/dbt-core
Compare commits
255 Commits
jerco/upda
...
v1.7.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c723cb1f7a | ||
|
|
6517c75e7e | ||
|
|
f95cf55d33 | ||
|
|
ad37fc9534 | ||
|
|
169972f480 | ||
|
|
7b96509409 | ||
|
|
cfbba81938 | ||
|
|
1391363d53 | ||
|
|
0d8e4af2f0 | ||
|
|
e9ee761194 | ||
|
|
5ec34cd4cc | ||
|
|
bc494bbe80 | ||
|
|
3f3dda48f7 | ||
|
|
1f98991376 | ||
|
|
5f5ddd27ac | ||
|
|
6e331833b0 | ||
|
|
d338b3e065 | ||
|
|
9f0bcf5666 | ||
|
|
d65179b24e | ||
|
|
0ef8638ab4 | ||
|
|
fc2d16fd34 | ||
|
|
de35686532 | ||
|
|
cf3714d2c1 | ||
|
|
09f5bb3dcf | ||
|
|
f94bf2bba4 | ||
|
|
ae75cc3a62 | ||
|
|
1e45a751a5 | ||
|
|
b5885da54e | ||
|
|
73ebe53273 | ||
|
|
2a9c3689a4 | ||
|
|
67d8ce398f | ||
|
|
7eb6cdbbfb | ||
|
|
6ba3dc211c | ||
|
|
ce86238389 | ||
|
|
7d60d6361e | ||
|
|
bebd6ca0e1 | ||
|
|
66cb5a0e7c | ||
|
|
2401600e57 | ||
|
|
a8fcf77831 | ||
|
|
0995825793 | ||
|
|
eeaaec4de9 | ||
|
|
1cbab8079a | ||
|
|
333d793cf0 | ||
|
|
b6f0eac2cd | ||
|
|
5e13698b2b | ||
|
|
12c1cbbc64 | ||
|
|
ced70d55c3 | ||
|
|
1baebb423c | ||
|
|
462df8395e | ||
|
|
35f214d9db | ||
|
|
af0cbcb6a5 | ||
|
|
2e35426d11 | ||
|
|
bf10a29f06 | ||
|
|
a7e2d9bc40 | ||
|
|
a3777496b5 | ||
|
|
edf6aedc51 | ||
|
|
53845d0277 | ||
|
|
3d27483658 | ||
|
|
4f9bd0cb38 | ||
|
|
3f7f7de179 | ||
|
|
6461f5aacf | ||
|
|
339957b42c | ||
|
|
4391dc1a63 | ||
|
|
964e0e4e8a | ||
|
|
549dbf3390 | ||
|
|
70b2e15a25 | ||
|
|
bb249d612c | ||
|
|
17773bdb94 | ||
|
|
f30293359c | ||
|
|
0c85e6149f | ||
|
|
ec57d7af94 | ||
|
|
df791f729c | ||
|
|
c6ff3abecd | ||
|
|
eac13e3bd3 | ||
|
|
46ee3f3d9c | ||
|
|
5e1f0c5fbc | ||
|
|
c4f09b160a | ||
|
|
48c97e86dd | ||
|
|
416bc845ad | ||
|
|
408a78985a | ||
|
|
0c965c8115 | ||
|
|
f65e4b6940 | ||
|
|
a2d4424f92 | ||
|
|
997f839cd6 | ||
|
|
556fad50df | ||
|
|
bb4214b5c2 | ||
|
|
f17c1f3fe7 | ||
|
|
d4fe9a8ad4 | ||
|
|
2910aa29e4 | ||
|
|
89cc073ea8 | ||
|
|
aa86fdfe71 | ||
|
|
48e9ced781 | ||
|
|
7b02bd1f02 | ||
|
|
417fc2a735 | ||
|
|
317128f790 | ||
|
|
e3dfb09b10 | ||
|
|
d912654110 | ||
|
|
34ab4cf9be | ||
|
|
d597b80486 | ||
|
|
3f5ebe81b9 | ||
|
|
f52bd9287b | ||
|
|
f5baeeea1c | ||
|
|
3cc7044fb3 | ||
|
|
26c7675c28 | ||
|
|
8aaed0e29f | ||
|
|
5182e3c40c | ||
|
|
1e252c7664 | ||
|
|
05ef3b6e44 | ||
|
|
ad04012b63 | ||
|
|
c93cba4603 | ||
|
|
971669016f | ||
|
|
6c6f245914 | ||
|
|
b39eeb328c | ||
|
|
be94bf1f3c | ||
|
|
e24a952e98 | ||
|
|
89f20d12cf | ||
|
|
ebeb0f1154 | ||
|
|
d66fe214d9 | ||
|
|
75781503b8 | ||
|
|
9aff3ca274 | ||
|
|
7e2a08f3a5 | ||
|
|
a0e13561b1 | ||
|
|
7eedfcd274 | ||
|
|
da779ac77c | ||
|
|
adfa3226e3 | ||
|
|
e5e1a272ff | ||
|
|
d8e8a78368 | ||
|
|
7ae3de1fa0 | ||
|
|
72898c7211 | ||
|
|
fc1a14a0e3 | ||
|
|
f063e4e01c | ||
|
|
07372db906 | ||
|
|
48d04e8141 | ||
|
|
6234267242 | ||
|
|
1afbb87e99 | ||
|
|
d18a74ddb7 | ||
|
|
4d3c6d9c7c | ||
|
|
10f9724827 | ||
|
|
582faa129e | ||
|
|
4ec87a01e0 | ||
|
|
ff98685dd6 | ||
|
|
424f3d218a | ||
|
|
661623f9f7 | ||
|
|
49397b4d7b | ||
|
|
0553fd817c | ||
|
|
7ad971f720 | ||
|
|
f485c13035 | ||
|
|
c30b691164 | ||
|
|
d088d4493e | ||
|
|
770f804325 | ||
|
|
37a29073de | ||
|
|
17cd145f09 | ||
|
|
ac539fd5cf | ||
|
|
048553ddc3 | ||
|
|
dfe6b71fd9 | ||
|
|
18ee93ca3a | ||
|
|
cb4bc2d6e9 | ||
|
|
b0451806ef | ||
|
|
b514e4c249 | ||
|
|
8350dfead3 | ||
|
|
34e6edbb13 | ||
|
|
27be92903e | ||
|
|
9388030182 | ||
|
|
b7aee3f5a4 | ||
|
|
83ff38ab24 | ||
|
|
6603a44151 | ||
|
|
e69d4e7f14 | ||
|
|
506f65e880 | ||
|
|
41bb52762b | ||
|
|
8c98ef3e70 | ||
|
|
44d1e73b4f | ||
|
|
53794fbaba | ||
|
|
556b4043e9 | ||
|
|
424c636533 | ||
|
|
f63709260e | ||
|
|
991618dfc1 | ||
|
|
1af489b1cd | ||
|
|
a433c31d6e | ||
|
|
5814928e38 | ||
|
|
6130a6e1d0 | ||
|
|
7872f6a670 | ||
|
|
f230e418aa | ||
|
|
518eb73f88 | ||
|
|
5b6d21d7da | ||
|
|
410506f448 | ||
|
|
3cb44d37c0 | ||
|
|
f977ed7471 | ||
|
|
3f5617b569 | ||
|
|
fe9c875d32 | ||
|
|
23b16ad6d2 | ||
|
|
fdeccfaf24 | ||
|
|
fecde23da5 | ||
|
|
b1d931337e | ||
|
|
39542336b8 | ||
|
|
799588cada | ||
|
|
f392add4b8 | ||
|
|
49560bf2a2 | ||
|
|
44b3ed5ae9 | ||
|
|
6235145641 | ||
|
|
ff5cb7ba51 | ||
|
|
1e2b9ae962 | ||
|
|
8cab58d248 | ||
|
|
0d645c227f | ||
|
|
fb6c349677 | ||
|
|
eeb057085c | ||
|
|
121371f4a4 | ||
|
|
a32713198b | ||
|
|
a1b067c683 | ||
|
|
22c40a4766 | ||
|
|
bcf140b3c1 | ||
|
|
e3692a6a3d | ||
|
|
e7489383a2 | ||
|
|
70246c3f86 | ||
|
|
0796c84da5 | ||
|
|
718482fb02 | ||
|
|
a3fb66daa4 | ||
|
|
da34b80c26 | ||
|
|
ba5ab21140 | ||
|
|
65f41a1e36 | ||
|
|
0930c9c059 | ||
|
|
1d193a9ab9 | ||
|
|
3adc6dca61 | ||
|
|
36d9f841d6 | ||
|
|
48ad13de00 | ||
|
|
42935cce05 | ||
|
|
e77f1c3b0f | ||
|
|
388838aa99 | ||
|
|
d4d0990072 | ||
|
|
4210d17f14 | ||
|
|
fbd12e78c9 | ||
|
|
83d3421e72 | ||
|
|
8bcbf73aaa | ||
|
|
cc5f15885d | ||
|
|
20fdf55bf6 | ||
|
|
955dcec68b | ||
|
|
2b8564b16f | ||
|
|
57da3e51cd | ||
|
|
dede0e9747 | ||
|
|
35d2fc1158 | ||
|
|
c5267335a3 | ||
|
|
15c7b589c2 | ||
|
|
0ada5e8bf7 | ||
|
|
412ac8d1b9 | ||
|
|
5df501a281 | ||
|
|
3e4c61d020 | ||
|
|
cc39fe51b3 | ||
|
|
89cd24388d | ||
|
|
d5da0a8093 | ||
|
|
88ae1f8871 | ||
|
|
50b3d1deaa | ||
|
|
3b3def5b8a | ||
|
|
4f068a45ff | ||
|
|
23a9504a51 | ||
|
|
d0d4eba477 | ||
|
|
a3fab0b5a9 |
@@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 1.7.0a1
|
||||
current_version = 1.7.8
|
||||
parse = (?P<major>[\d]+) # major version number
|
||||
\.(?P<minor>[\d]+) # minor version number
|
||||
\.(?P<patch>[\d]+) # patch version number
|
||||
|
||||
157
.changes/1.7.0.md
Normal file
157
.changes/1.7.0.md
Normal file
@@ -0,0 +1,157 @@
|
||||
## dbt-core 1.7.0 - November 02, 2023
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed the FirstRunResultError and AfterFirstRunResultError event types, using the existing RunResultError in their place. ([#7963](https://github.com/dbt-labs/dbt-core/issues/7963))
|
||||
|
||||
### Features
|
||||
|
||||
- add log file of installed packages via dbt deps ([#6643](https://github.com/dbt-labs/dbt-core/issues/6643))
|
||||
- Enable re-population of metadata vars post-environment change during programmatic invocation ([#8010](https://github.com/dbt-labs/dbt-core/issues/8010))
|
||||
- Added support to configure a delimiter for a seed file, defaults to comma ([#3990](https://github.com/dbt-labs/dbt-core/issues/3990))
|
||||
- Allow specification of `create_metric: true` on measures ([#8125](https://github.com/dbt-labs/dbt-core/issues/8125))
|
||||
- Add node attributes related to compilation to run_results.json ([#7519](https://github.com/dbt-labs/dbt-core/issues/7519))
|
||||
- Add --no-inject-ephemeral-ctes flag for `compile` command, for usage by linting. ([#8480](https://github.com/dbt-labs/dbt-core/issues/8480))
|
||||
- Support configuration of semantic models with the addition of enable/disable and group enablement. ([#7968](https://github.com/dbt-labs/dbt-core/issues/7968))
|
||||
- Accept a `dbt-cloud` config in dbt_project.yml ([#8438](https://github.com/dbt-labs/dbt-core/issues/8438))
|
||||
- Support atomic replace in the global replace macro ([#8539](https://github.com/dbt-labs/dbt-core/issues/8539))
|
||||
- Use translate_type on data_type in model.columns in templates by default, remove no op `TYPE_LABELS` ([#8007](https://github.com/dbt-labs/dbt-core/issues/8007))
|
||||
- Add an option to generate static documentation ([#8614](https://github.com/dbt-labs/dbt-core/issues/8614))
|
||||
- Allow setting "access" as a config in addition to as a property ([#8383](https://github.com/dbt-labs/dbt-core/issues/8383))
|
||||
- Loosen typing requirement on renameable/replaceable relations to Iterable to allow adapters more flexibility in registering relation types, include docstrings as suggestions ([#8647](https://github.com/dbt-labs/dbt-core/issues/8647))
|
||||
- Add support for optional label in semantic_models, measures, dimensions and entities. ([#8595](https://github.com/dbt-labs/dbt-core/issues/8595), [#8755](https://github.com/dbt-labs/dbt-core/issues/8755))
|
||||
- Allow adapters to include package logs in dbt standard logging ([#7859](https://github.com/dbt-labs/dbt-core/issues/7859))
|
||||
- Support storing test failures as views ([#6914](https://github.com/dbt-labs/dbt-core/issues/6914))
|
||||
- resolve packages with same git repo and unique subdirectory ([#5374](https://github.com/dbt-labs/dbt-core/issues/5374))
|
||||
- Add new ResourceReport event to record memory/cpu/io metrics ([#8342](https://github.com/dbt-labs/dbt-core/issues/8342))
|
||||
- Adding `date_spine` macro (and supporting macros) from dbt-utils to dbt-core ([#8172](https://github.com/dbt-labs/dbt-core/issues/8172))
|
||||
- Support `fill_nulls_with` and `join_to_timespine` for metric nodes ([#8593](https://github.com/dbt-labs/dbt-core/issues/8593), [#8755](https://github.com/dbt-labs/dbt-core/issues/8755))
|
||||
- Raise a warning when a contracted model has a numeric field without scale defined ([#8183](https://github.com/dbt-labs/dbt-core/issues/8183))
|
||||
- Added support for retrieving partial catalog information from a schema ([#8521](https://github.com/dbt-labs/dbt-core/issues/8521))
|
||||
- Add meta attribute to SemanticModels config ([#8511](https://github.com/dbt-labs/dbt-core/issues/8511))
|
||||
- Selectors with docs generate limits catalog generation ([#6014](https://github.com/dbt-labs/dbt-core/issues/6014))
|
||||
- Allow freshness to be determined via DBMS metadata for supported adapters ([#8704](https://github.com/dbt-labs/dbt-core/issues/8704))
|
||||
- Add support semantic layer SavedQuery node type ([#8594](https://github.com/dbt-labs/dbt-core/issues/8594))
|
||||
- Add exports to SavedQuery spec ([#8892](https://github.com/dbt-labs/dbt-core/issues/8892))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Copy dir during `dbt deps` if symlink fails ([#7428](https://github.com/dbt-labs/dbt-core/issues/7428), [#8223](https://github.com/dbt-labs/dbt-core/issues/8223))
|
||||
- If --profile specified with dbt-init, create the project with the specified profile ([#6154](https://github.com/dbt-labs/dbt-core/issues/6154))
|
||||
- Fixed double-underline ([#5301](https://github.com/dbt-labs/dbt-core/issues/5301))
|
||||
- Copy target_schema from config into snapshot node ([#6745](https://github.com/dbt-labs/dbt-core/issues/6745))
|
||||
- Enable converting deprecation warnings to errors ([#8130](https://github.com/dbt-labs/dbt-core/issues/8130))
|
||||
- Add status to Parse Inline Error ([#8173](https://github.com/dbt-labs/dbt-core/issues/8173))
|
||||
- Ensure `warn_error_options` get serialized in `invocation_args_dict` ([#7694](https://github.com/dbt-labs/dbt-core/issues/7694))
|
||||
- Stop detecting materialization macros based on macro name ([#6231](https://github.com/dbt-labs/dbt-core/issues/6231))
|
||||
- Update `dbt deps` download retry logic to handle `EOFError` exceptions ([#6653](https://github.com/dbt-labs/dbt-core/issues/6653))
|
||||
- Improve handling of CTE injection with ephemeral models ([#8213](https://github.com/dbt-labs/dbt-core/issues/8213))
|
||||
- Fix unbound local variable error in `checked_agg_time_dimension_for_measure` ([#8230](https://github.com/dbt-labs/dbt-core/issues/8230))
|
||||
- Ensure runtime errors are raised for graph runnable tasks (compile, show, run, etc) ([#8166](https://github.com/dbt-labs/dbt-core/issues/8166))
|
||||
- Fix retry not working with log-file-max-bytes ([#8297](https://github.com/dbt-labs/dbt-core/issues/8297))
|
||||
- Add explicit support for integers for the show command ([#8153](https://github.com/dbt-labs/dbt-core/issues/8153))
|
||||
- Detect changes to model access, version, or latest_version in state:modified ([#8189](https://github.com/dbt-labs/dbt-core/issues/8189))
|
||||
- Add connection status into list of statuses for dbt debug ([#8350](https://github.com/dbt-labs/dbt-core/issues/8350))
|
||||
- fix fqn-selection for external versioned models ([#8374](https://github.com/dbt-labs/dbt-core/issues/8374))
|
||||
- Fix: DbtInternalError after model that previously ref'd external model is deleted ([#8375](https://github.com/dbt-labs/dbt-core/issues/8375))
|
||||
- Fix using list command with path selector and project-dir ([#8385](https://github.com/dbt-labs/dbt-core/issues/8385))
|
||||
- Remedy performance regression by only writing run_results.json once. ([#8360](https://github.com/dbt-labs/dbt-core/issues/8360))
|
||||
- Add support for swapping materialized views with tables/views and vice versa ([#8449](https://github.com/dbt-labs/dbt-core/issues/8449))
|
||||
- Turn breaking changes to contracted models into warnings for unversioned models ([#8384](https://github.com/dbt-labs/dbt-core/issues/8384), [#8282](https://github.com/dbt-labs/dbt-core/issues/8282))
|
||||
- Ensure parsing does not break when `window_groupings` is not specified for `non_additive_dimension` ([#8453](https://github.com/dbt-labs/dbt-core/issues/8453))
|
||||
- fix ambiguous reference error for tests and versions when model name is duplicated across packages ([#8327](https://github.com/dbt-labs/dbt-core/issues/8327), [#8493](https://github.com/dbt-labs/dbt-core/issues/8493))
|
||||
- Fix "Internal Error: Expected node <unique-id> not found in manifest" when depends_on set on ModelNodeArgs ([#8506](https://github.com/dbt-labs/dbt-core/issues/8506))
|
||||
- Fix snapshot success message ([#7583](https://github.com/dbt-labs/dbt-core/issues/7583))
|
||||
- Parse the correct schema version from manifest ([#8544](https://github.com/dbt-labs/dbt-core/issues/8544))
|
||||
- make version comparison insensitive to order ([#8571](https://github.com/dbt-labs/dbt-core/issues/8571))
|
||||
- Update metric helper functions to work with new semantic layer metrics ([#8134](https://github.com/dbt-labs/dbt-core/issues/8134))
|
||||
- Disallow cleaning paths outside current working directory ([#8318](https://github.com/dbt-labs/dbt-core/issues/8318))
|
||||
- Warn when --state == --target ([#8160](https://github.com/dbt-labs/dbt-core/issues/8160))
|
||||
- update dbt show to include limit in DWH query ([#8496,](https://github.com/dbt-labs/dbt-core/issues/8496,), [#8417](https://github.com/dbt-labs/dbt-core/issues/8417))
|
||||
- Support quoted parameter list for MultiOption CLI options. ([#8598](https://github.com/dbt-labs/dbt-core/issues/8598))
|
||||
- Support global flags passed in after subcommands ([#6497](https://github.com/dbt-labs/dbt-core/issues/6497))
|
||||
- Lower bound of `8.0.2` for `click` ([#8683](https://github.com/dbt-labs/dbt-core/issues/8683))
|
||||
- Fixes test type edges filter ([#8692](https://github.com/dbt-labs/dbt-core/issues/8692))
|
||||
- semantic models in graph selection ([#8589](https://github.com/dbt-labs/dbt-core/issues/8589))
|
||||
- Support doc blocks in nested semantic model YAML ([#8509](https://github.com/dbt-labs/dbt-core/issues/8509))
|
||||
- avoid double-rendering sql_header in dbt show ([#8739](https://github.com/dbt-labs/dbt-core/issues/8739))
|
||||
- Fix tag selection for projects with semantic models ([#8749](https://github.com/dbt-labs/dbt-core/issues/8749))
|
||||
- Foreign key constraint on incremental model results in Database Error ([#8022](https://github.com/dbt-labs/dbt-core/issues/8022))
|
||||
- Support docs blocks on versioned model column descriptions ([#8540](https://github.com/dbt-labs/dbt-core/issues/8540))
|
||||
- Enable seeds to be handled from stored manifest data ([#6875](https://github.com/dbt-labs/dbt-core/issues/6875))
|
||||
- Override path-like args in dbt retry ([#8682](https://github.com/dbt-labs/dbt-core/issues/8682))
|
||||
- Group updates on unmodified nodes are handled gracefully for state:modified ([#8371](https://github.com/dbt-labs/dbt-core/issues/8371))
|
||||
- Partial parsing fix for adding groups and updating models at the same time ([#8697](https://github.com/dbt-labs/dbt-core/issues/8697))
|
||||
- Fix partial parsing not working for semantic model change ([#8859](https://github.com/dbt-labs/dbt-core/issues/8859))
|
||||
- Rework get_catalog implementation to retain previous adapter interface semantics ([#8846](https://github.com/dbt-labs/dbt-core/issues/8846))
|
||||
- Add back contract enforcement for temporary tables on postgres ([#8857](https://github.com/dbt-labs/dbt-core/issues/8857))
|
||||
- Add version to fqn when version==0 ([#8836](https://github.com/dbt-labs/dbt-core/issues/8836))
|
||||
- Fix cased comparison in catalog-retrieval function. ([#8939](https://github.com/dbt-labs/dbt-core/issues/8939))
|
||||
- Catalog queries now assign the correct type to materialized views ([#8864](https://github.com/dbt-labs/dbt-core/issues/8864))
|
||||
- Make relation filtering None-tolerant for maximal flexibility across adapters. ([#8974](https://github.com/dbt-labs/dbt-core/issues/8974))
|
||||
|
||||
### Docs
|
||||
|
||||
- Corrected spelling of "Partiton" ([dbt-docs/#8100](https://github.com/dbt-labs/dbt-docs/issues/8100))
|
||||
- Remove static SQL codeblock for metrics ([dbt-docs/#436](https://github.com/dbt-labs/dbt-docs/issues/436))
|
||||
- fixed comment util.py ([dbt-docs/#None](https://github.com/dbt-labs/dbt-docs/issues/None))
|
||||
- Fix newline escapes and improve formatting in docker README ([dbt-docs/#8211](https://github.com/dbt-labs/dbt-docs/issues/8211))
|
||||
- Display contract and column constraints on the model page ([dbt-docs/#433](https://github.com/dbt-labs/dbt-docs/issues/433))
|
||||
- Display semantic model details in docs ([dbt-docs/#431](https://github.com/dbt-labs/dbt-docs/issues/431))
|
||||
|
||||
### Under the Hood
|
||||
|
||||
- Switch from hologram to mashumaro jsonschema ([#8426](https://github.com/dbt-labs/dbt-core/issues/8426))
|
||||
- Refactor flaky test pp_versioned_models ([#7781](https://github.com/dbt-labs/dbt-core/issues/7781))
|
||||
- format exception from dbtPlugin.initialize ([#8152](https://github.com/dbt-labs/dbt-core/issues/8152))
|
||||
- A way to control maxBytes for a single dbt.log file ([#8199](https://github.com/dbt-labs/dbt-core/issues/8199))
|
||||
- Ref expressions with version can now be processed by the latest version of the high-performance dbt-extractor library. ([#7688](https://github.com/dbt-labs/dbt-core/issues/7688))
|
||||
- Bump manifest schema version to v11, freeze manifest v10 ([#8333](https://github.com/dbt-labs/dbt-core/issues/8333))
|
||||
- add tracking for plugin.get_nodes calls ([#8344](https://github.com/dbt-labs/dbt-core/issues/8344))
|
||||
- add internal flag: --no-partial-parse-file-diff to inform whether to compute a file diff during partial parsing ([#8363](https://github.com/dbt-labs/dbt-core/issues/8363))
|
||||
- Add return values to a number of functions for mypy ([#8389](https://github.com/dbt-labs/dbt-core/issues/8389))
|
||||
- Fix mypy warnings for ManifestLoader.load() ([#8401](https://github.com/dbt-labs/dbt-core/issues/8401))
|
||||
- Use python version 3.10.7 in Docker image. ([#8444](https://github.com/dbt-labs/dbt-core/issues/8444))
|
||||
- Re-organize jinja macros: relation-specific in /macros/adapters/relations/<relation>, relation agnostic in /macros/relations ([#8449](https://github.com/dbt-labs/dbt-core/issues/8449))
|
||||
- Update typing to meet mypy standards ([#8396](https://github.com/dbt-labs/dbt-core/issues/8396))
|
||||
- Mypy errors - adapters/factory.py ([#8387](https://github.com/dbt-labs/dbt-core/issues/8387))
|
||||
- Added more type annotations. ([#8537](https://github.com/dbt-labs/dbt-core/issues/8537))
|
||||
- Audit potential circular dependencies ([#8349](https://github.com/dbt-labs/dbt-core/issues/8349))
|
||||
- Add functional test for advanced ref override ([#8566](https://github.com/dbt-labs/dbt-core/issues/8566))
|
||||
- Add typing to __init__ in base.py ([#8398](https://github.com/dbt-labs/dbt-core/issues/8398))
|
||||
- Fix untyped functions in task/runnable.py (mypy warning) ([#8402](https://github.com/dbt-labs/dbt-core/issues/8402))
|
||||
- add a test for ephemeral cte injection ([#8225](https://github.com/dbt-labs/dbt-core/issues/8225))
|
||||
- Fix test_numeric_values to look for more specific strings ([#8470](https://github.com/dbt-labs/dbt-core/issues/8470))
|
||||
- Pin types-requests<2.31.0 in `dev-requirements.txt` ([#8789](https://github.com/dbt-labs/dbt-core/issues/8789))
|
||||
- Add warning_tag to UnversionedBreakingChange ([#8827](https://github.com/dbt-labs/dbt-core/issues/8827))
|
||||
- Update v10 manifest schema to match 1.6 for testing schema compatibility ([#8835](https://github.com/dbt-labs/dbt-core/issues/8835))
|
||||
- Add a no-op runner for Saved Qeury ([#8893](https://github.com/dbt-labs/dbt-core/issues/8893))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Bump mypy from 1.3.0 to 1.4.0 ([#7912](https://github.com/dbt-labs/dbt-core/pull/7912))
|
||||
- Bump mypy from 1.4.0 to 1.4.1 ([#8219](https://github.com/dbt-labs/dbt-core/pull/8219))
|
||||
- Update pin for click<9 ([#8232](https://github.com/dbt-labs/dbt-core/pull/8232))
|
||||
- Add upper bound to sqlparse pin of <0.5 ([#8236](https://github.com/dbt-labs/dbt-core/pull/8236))
|
||||
- Support dbt-semantic-interfaces 0.2.0 ([#8250](https://github.com/dbt-labs/dbt-core/pull/8250))
|
||||
- Bump docker/build-push-action from 4 to 5 ([#8783](https://github.com/dbt-labs/dbt-core/pull/8783))
|
||||
- Upgrade dbt-semantic-interfaces dep to 0.3.0 ([#8819](https://github.com/dbt-labs/dbt-core/pull/8819))
|
||||
- Begin using DSI 0.4.x ([#8892](https://github.com/dbt-labs/dbt-core/pull/8892))
|
||||
|
||||
### Contributors
|
||||
- [@anjutiwari](https://github.com/anjutiwari) ([#7428](https://github.com/dbt-labs/dbt-core/issues/7428), [#8223](https://github.com/dbt-labs/dbt-core/issues/8223))
|
||||
- [@benmosher](https://github.com/benmosher) ([#8480](https://github.com/dbt-labs/dbt-core/issues/8480))
|
||||
- [@d-kaneshiro](https://github.com/d-kaneshiro) ([#None](https://github.com/dbt-labs/dbt-core/issues/None))
|
||||
- [@dave-connors-3](https://github.com/dave-connors-3) ([#8153](https://github.com/dbt-labs/dbt-core/issues/8153), [#8589](https://github.com/dbt-labs/dbt-core/issues/8589))
|
||||
- [@dylan-murray](https://github.com/dylan-murray) ([#8683](https://github.com/dbt-labs/dbt-core/issues/8683))
|
||||
- [@ezraerb](https://github.com/ezraerb) ([#6154](https://github.com/dbt-labs/dbt-core/issues/6154))
|
||||
- [@gem7318](https://github.com/gem7318) ([#8010](https://github.com/dbt-labs/dbt-core/issues/8010))
|
||||
- [@jamezrin](https://github.com/jamezrin) ([#8211](https://github.com/dbt-labs/dbt-core/issues/8211))
|
||||
- [@jusbaldw](https://github.com/jusbaldw) ([#6643](https://github.com/dbt-labs/dbt-core/issues/6643))
|
||||
- [@lllong33](https://github.com/lllong33) ([#5301](https://github.com/dbt-labs/dbt-core/issues/5301))
|
||||
- [@marcodamore](https://github.com/marcodamore) ([#436](https://github.com/dbt-labs/dbt-core/issues/436))
|
||||
- [@mescanne](https://github.com/mescanne) ([#8614](https://github.com/dbt-labs/dbt-core/issues/8614))
|
||||
- [@pgoslatara](https://github.com/pgoslatara) ([#8100](https://github.com/dbt-labs/dbt-core/issues/8100))
|
||||
- [@philippeboyd](https://github.com/philippeboyd) ([#5374](https://github.com/dbt-labs/dbt-core/issues/5374))
|
||||
- [@ramonvermeulen](https://github.com/ramonvermeulen) ([#3990](https://github.com/dbt-labs/dbt-core/issues/3990))
|
||||
- [@renanleme](https://github.com/renanleme) ([#8692](https://github.com/dbt-labs/dbt-core/issues/8692))
|
||||
8
.changes/1.7.1.md
Normal file
8
.changes/1.7.1.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## dbt-core 1.7.1 - November 07, 2023
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix compilation exception running empty seed file and support new Integer agate data_type ([#8895](https://github.com/dbt-labs/dbt-core/issues/8895))
|
||||
- Update run_results.json from previous versions of dbt to support deferral and rerun from failure ([#9010](https://github.com/dbt-labs/dbt-core/issues/9010))
|
||||
- Use MANIFEST.in to recursively include all jinja templates; fixes issue where some templates were not included in the distribution ([#9016](https://github.com/dbt-labs/dbt-core/issues/9016))
|
||||
- Fix git repository with subdirectory for Deps ([#9000](https://github.com/dbt-labs/dbt-core/issues/9000))
|
||||
16
.changes/1.7.2.md
Normal file
16
.changes/1.7.2.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## dbt-core 1.7.2 - November 16, 2023
|
||||
|
||||
### Features
|
||||
|
||||
- Support setting export configs hierarchically via saved query and project configs ([#8956](https://github.com/dbt-labs/dbt-core/issues/8956))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix formatting of tarball information in packages-lock.yml ([#9062](https://github.com/dbt-labs/dbt-core/issues/9062))
|
||||
|
||||
### Under the Hood
|
||||
|
||||
- Treat SystemExit as an interrupt if raised during node execution. ([#n/a](https://github.com/dbt-labs/dbt-core/issues/n/a))
|
||||
|
||||
### Contributors
|
||||
- [@benmosher](https://github.com/benmosher) ([#n/a](https://github.com/dbt-labs/dbt-core/issues/n/a))
|
||||
7
.changes/1.7.3.md
Normal file
7
.changes/1.7.3.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## dbt-core 1.7.3 - November 29, 2023
|
||||
|
||||
### Fixes
|
||||
|
||||
- deps: Lock git packages to commit SHA during resolution ([#9050](https://github.com/dbt-labs/dbt-core/issues/9050))
|
||||
- deps: Use PackageRenderer to read package-lock.json ([#9127](https://github.com/dbt-labs/dbt-core/issues/9127))
|
||||
- Get sources working again in dbt docs generate ([#9119](https://github.com/dbt-labs/dbt-core/issues/9119))
|
||||
12
.changes/1.7.4.md
Normal file
12
.changes/1.7.4.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## dbt-core 1.7.4 - December 14, 2023
|
||||
|
||||
### Features
|
||||
|
||||
- Adds support for parsing conversion metric related properties for the semantic layer. ([#9203](https://github.com/dbt-labs/dbt-core/issues/9203))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Ensure we produce valid jsonschema schemas for manifest, catalog, run-results, and sources ([#8991](https://github.com/dbt-labs/dbt-core/issues/8991))
|
||||
|
||||
### Contributors
|
||||
- [@WilliamDee](https://github.com/WilliamDee) ([#9203](https://github.com/dbt-labs/dbt-core/issues/9203))
|
||||
8
.changes/1.7.5.md
Normal file
8
.changes/1.7.5.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## dbt-core 1.7.5 - January 18, 2024
|
||||
|
||||
### Fixes
|
||||
|
||||
- Preserve the value of vars and the --full-refresh flags when using retry. ([#9112](https://github.com/dbt-labs/dbt-core/issues/9112))
|
||||
|
||||
### Contributors
|
||||
- [@peterallenwebb,](https://github.com/peterallenwebb,) ([#9112](https://github.com/dbt-labs/dbt-core/issues/9112))
|
||||
6
.changes/1.7.6.md
Normal file
6
.changes/1.7.6.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## dbt-core 1.7.6 - January 25, 2024
|
||||
|
||||
### Fixes
|
||||
|
||||
- Handle unknown `type_code` for model contracts ([#8877](https://github.com/dbt-labs/dbt-core/issues/8877), [#8353](https://github.com/dbt-labs/dbt-core/issues/8353))
|
||||
- Fix retry command run from CLI ([#9444](https://github.com/dbt-labs/dbt-core/issues/9444))
|
||||
6
.changes/1.7.7.md
Normal file
6
.changes/1.7.7.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## dbt-core 1.7.7 - February 01, 2024
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix seed and source selection in `dbt docs generate` ([#9161](https://github.com/dbt-labs/dbt-core/issues/9161))
|
||||
- Add TestGenerateCatalogWithExternalNodes, include empty nodes in node selection during docs generate ([#9456](https://github.com/dbt-labs/dbt-core/issues/9456))
|
||||
6
.changes/1.7.8.md
Normal file
6
.changes/1.7.8.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## dbt-core 1.7.8 - February 14, 2024
|
||||
|
||||
### Fixes
|
||||
|
||||
- When patching versioned models, set constraints after config ([#9364](https://github.com/dbt-labs/dbt-core/issues/9364))
|
||||
- Store node_info in node associated logging events ([#9557](https://github.com/dbt-labs/dbt-core/issues/9557))
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: Docs
|
||||
body: Fix for column tests not rendering on quoted columns
|
||||
time: 2023-05-31T11:54:19.687363-04:00
|
||||
custom:
|
||||
Author: drewbanin
|
||||
Issue: "201"
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: Docs
|
||||
body: Remove static SQL codeblock for metrics
|
||||
time: 2023-07-18T19:24:22.155323+02:00
|
||||
custom:
|
||||
Author: marcodamore
|
||||
Issue: "436"
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: Fixes
|
||||
body: Enable converting deprecation warnings to errors
|
||||
time: 2023-07-18T12:55:18.03914-04:00
|
||||
custom:
|
||||
Author: michelleark
|
||||
Issue: "8130"
|
||||
19
.github/CODEOWNERS
vendored
19
.github/CODEOWNERS
vendored
@@ -13,23 +13,6 @@
|
||||
# the core team as a whole will be assigned
|
||||
* @dbt-labs/core-team
|
||||
|
||||
### OSS Tooling Guild
|
||||
|
||||
/.github/ @dbt-labs/guild-oss-tooling
|
||||
.bumpversion.cfg @dbt-labs/guild-oss-tooling
|
||||
|
||||
.changie.yaml @dbt-labs/guild-oss-tooling
|
||||
|
||||
pre-commit-config.yaml @dbt-labs/guild-oss-tooling
|
||||
pytest.ini @dbt-labs/guild-oss-tooling
|
||||
tox.ini @dbt-labs/guild-oss-tooling
|
||||
|
||||
pyproject.toml @dbt-labs/guild-oss-tooling
|
||||
requirements.txt @dbt-labs/guild-oss-tooling
|
||||
dev_requirements.txt @dbt-labs/guild-oss-tooling
|
||||
/core/setup.py @dbt-labs/guild-oss-tooling
|
||||
/core/MANIFEST.in @dbt-labs/guild-oss-tooling
|
||||
|
||||
### ADAPTERS
|
||||
|
||||
# Adapter interface ("base" + "sql" adapter defaults, cache)
|
||||
@@ -40,7 +23,7 @@ dev_requirements.txt @dbt-labs/guild-oss-tooling
|
||||
|
||||
# Postgres plugin
|
||||
/plugins/ @dbt-labs/core-adapters
|
||||
/plugins/postgres/setup.py @dbt-labs/core-adapters @dbt-labs/guild-oss-tooling
|
||||
/plugins/postgres/setup.py @dbt-labs/core-adapters
|
||||
|
||||
# Functional tests for adapter plugins
|
||||
/tests/adapter @dbt-labs/core-adapters
|
||||
|
||||
58
.github/ISSUE_TEMPLATE/implementation-ticket.yml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/implementation-ticket.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: 🛠️ Implementation
|
||||
description: This is an implementation ticket intended for use by the maintainers of dbt-core
|
||||
title: "[<project>] <title>"
|
||||
labels: ["user docs"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: This is an implementation ticket intended for use by the maintainers of dbt-core
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Housekeeping
|
||||
description: >
|
||||
A couple friendly reminders:
|
||||
1. Remove the `user docs` label if the scope of this work does not require changes to https://docs.getdbt.com/docs: no end-user interface (e.g. yml spec, CLI, error messages, etc) or functional changes
|
||||
2. Link any blocking issues in the "Blocked on" field under the "Core devs & maintainers" project.
|
||||
options:
|
||||
- label: I am a maintainer of dbt-core
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Short description
|
||||
description: |
|
||||
Describe the scope of the ticket, a high-level implementation approach and any tradeoffs to consider
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Acceptance criteria
|
||||
description: |
|
||||
What is the definition of done for this ticket? Include any relevant edge cases and/or test cases
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Impact to Other Teams
|
||||
description: |
|
||||
Will this change impact other teams? Include details of the kinds of changes required (new tests, code changes, related tickets) and _add the relevant `Impact:[team]` label_.
|
||||
placeholder: |
|
||||
Example: This change impacts `dbt-redshift` because the tests will need to be modified. The `Impact:[Adapter]` label has been added.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Will backports be required?
|
||||
description: |
|
||||
Will this change need to be backported to previous versions? Add details, possible blockers to backporting and _add the relevant backport labels `backport 1.x.latest`_
|
||||
placeholder: |
|
||||
Example: Backport to 1.6.latest, 1.5.latest and 1.4.latest. Since 1.4 isn't using click, the backport may be complicated. The `backport 1.6.latest`, `backport 1.5.latest` and `backport 1.4.latest` labels have been added.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Context
|
||||
description: |
|
||||
Provide the "why", motivation, and alternative approaches considered -- linking to previous refinement issues, spikes, Notion docs as appropriate
|
||||
validations:
|
||||
validations:
|
||||
required: false
|
||||
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -28,3 +28,10 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "disabled"
|
||||
|
||||
# github dependencies
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "disabled"
|
||||
|
||||
10
.github/pull_request_template.md
vendored
10
.github/pull_request_template.md
vendored
@@ -1,15 +1,12 @@
|
||||
resolves #
|
||||
[docs](https://github.com/dbt-labs/docs.getdbt.com/issues/new/choose) dbt-labs/docs.getdbt.com/#
|
||||
resolves #
|
||||
|
||||
<!---
|
||||
Include the number of the issue addressed by this PR above if applicable.
|
||||
PRs for code changes without an associated issue *will not be merged*.
|
||||
See CONTRIBUTING.md for more information.
|
||||
|
||||
Include the number of the docs issue that was opened for this PR. If
|
||||
this change has no user-facing implications, "N/A" suffices instead. New
|
||||
docs tickets can be created by clicking the link above or by going to
|
||||
https://github.com/dbt-labs/docs.getdbt.com/issues/new/choose.
|
||||
Add the `user docs` label to this PR if it will need docs changes. An
|
||||
issue will get opened in docs.getdbt.com upon successful merge of this PR.
|
||||
-->
|
||||
|
||||
### Problem
|
||||
@@ -33,3 +30,4 @@ resolves #
|
||||
- [ ] I have run this code in development and it appears to resolve the stated issue
|
||||
- [ ] This PR includes tests, or tests are not required/relevant for this PR
|
||||
- [ ] This PR has no interface changes (e.g. macros, cli, logs, json artifacts, config files, adapter interface, etc) or this PR has already received feedback and approval from Product or DX
|
||||
- [ ] This PR includes [type annotations](https://docs.python.org/3/library/typing.html) for new and modified functions
|
||||
|
||||
8
.github/workflows/changelog-existence.yml
vendored
8
.github/workflows/changelog-existence.yml
vendored
@@ -2,10 +2,8 @@
|
||||
# Checks that a file has been committed under the /.changes directory
|
||||
# as a new CHANGELOG entry. Cannot check for a specific filename as
|
||||
# it is dynamically generated by change type and timestamp.
|
||||
# This workflow should not require any secrets since it runs for PRs
|
||||
# from forked repos.
|
||||
# By default, secrets are not passed to workflows running from
|
||||
# a forked repo.
|
||||
# This workflow runs on pull_request_target because it requires
|
||||
# secrets to post comments.
|
||||
|
||||
# **why?**
|
||||
# Ensure code change gets reflected in the CHANGELOG.
|
||||
@@ -19,7 +17,7 @@
|
||||
name: Check Changelog Entry
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, labeled, unlabeled, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
43
.github/workflows/docs-issue.yml
vendored
Normal file
43
.github/workflows/docs-issue.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# **what?**
|
||||
# Open an issue in docs.getdbt.com when a PR is labeled `user docs`
|
||||
|
||||
# **why?**
|
||||
# To reduce barriers for keeping docs up to date
|
||||
|
||||
# **when?**
|
||||
# When a PR is labeled `user docs` and is merged. Runs on pull_request_target to run off the workflow already merged,
|
||||
# not the workflow that existed on the PR branch. This allows old PRs to get comments.
|
||||
|
||||
|
||||
name: Open issues in docs.getdbt.com repo when a PR is labeled
|
||||
run-name: "Open an issue in docs.getdbt.com for PR #${{ github.event.pull_request.number }}"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled, closed]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
permissions:
|
||||
issues: write # opens new issues
|
||||
pull-requests: write # comments on PRs
|
||||
|
||||
|
||||
jobs:
|
||||
open_issues:
|
||||
# we only want to run this when the PR has been merged or the label in the labeled event is `user docs`. Otherwise it runs the
|
||||
# risk of duplicaton of issues being created due to merge and label both triggering this workflow to run and neither having
|
||||
# generating the comment before the other runs. This lives here instead of the shared workflow because this is where we
|
||||
# decide if it should run or not.
|
||||
if: |
|
||||
(github.event.pull_request.merged == true) &&
|
||||
((github.event.action == 'closed' && contains( github.event.pull_request.labels.*.name, 'user docs')) ||
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'user docs'))
|
||||
uses: dbt-labs/actions/.github/workflows/open-issue-in-repo.yml@main
|
||||
with:
|
||||
issue_repository: "dbt-labs/docs.getdbt.com"
|
||||
issue_title: "Docs Changes Needed from ${{ github.event.repository.name }} PR #${{ github.event.pull_request.number }}"
|
||||
issue_body: "At a minimum, update body to include a link to the page on docs.getdbt.com requiring updates and what part(s) of the page you would like to see updated."
|
||||
secrets: inherit
|
||||
84
.github/workflows/main.yml
vendored
84
.github/workflows/main.yml
vendored
@@ -33,6 +33,11 @@ defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
# top-level adjustments can be made here
|
||||
env:
|
||||
# number of parallel processes to spawn for python integration testing
|
||||
PYTHON_INTEGRATION_TEST_WORKERS: 5
|
||||
|
||||
jobs:
|
||||
code-quality:
|
||||
name: code-quality
|
||||
@@ -103,26 +108,59 @@ jobs:
|
||||
- name: Upload Unit Test Coverage to Codecov
|
||||
if: ${{ matrix.python-version == '3.11' }}
|
||||
uses: codecov/codecov-action@v3
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: unit
|
||||
|
||||
integration-metadata:
|
||||
name: integration test metadata generation
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
split-groups: ${{ steps.generate-split-groups.outputs.split-groups }}
|
||||
include: ${{ steps.generate-include.outputs.include }}
|
||||
|
||||
steps:
|
||||
- name: generate split-groups
|
||||
id: generate-split-groups
|
||||
run: |
|
||||
MATRIX_JSON="["
|
||||
for B in $(seq 1 ${{ env.PYTHON_INTEGRATION_TEST_WORKERS }}); do
|
||||
MATRIX_JSON+=$(sed 's/^/"/;s/$/"/' <<< "${B}")
|
||||
done
|
||||
MATRIX_JSON="${MATRIX_JSON//\"\"/\", \"}"
|
||||
MATRIX_JSON+="]"
|
||||
echo "split-groups=${MATRIX_JSON}"
|
||||
echo "split-groups=${MATRIX_JSON}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: generate include
|
||||
id: generate-include
|
||||
run: |
|
||||
INCLUDE=('"python-version":"3.8","os":"windows-latest"' '"python-version":"3.8","os":"macos-latest"' )
|
||||
INCLUDE_GROUPS="["
|
||||
for include in ${INCLUDE[@]}; do
|
||||
for group in $(seq 1 ${{ env.PYTHON_INTEGRATION_TEST_WORKERS }}); do
|
||||
INCLUDE_GROUPS+=$(sed 's/$/, /' <<< "{\"split-group\":\"${group}\",${include}}")
|
||||
done
|
||||
done
|
||||
INCLUDE_GROUPS=$(echo $INCLUDE_GROUPS | sed 's/,*$//g')
|
||||
INCLUDE_GROUPS+="]"
|
||||
echo "include=${INCLUDE_GROUPS}"
|
||||
echo "include=${INCLUDE_GROUPS}" >> $GITHUB_OUTPUT
|
||||
|
||||
integration:
|
||||
name: integration test / python ${{ matrix.python-version }} / ${{ matrix.os }}
|
||||
name: (${{ matrix.split-group }}) integration test / python ${{ matrix.python-version }} / ${{ matrix.os }}
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- integration-metadata
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
os: [ubuntu-20.04]
|
||||
include:
|
||||
- python-version: 3.8
|
||||
os: windows-latest
|
||||
- python-version: 3.8
|
||||
os: macos-latest
|
||||
|
||||
split-group: ${{ fromJson(needs.integration-metadata.outputs.split-groups) }}
|
||||
include: ${{ fromJson(needs.integration-metadata.outputs.include) }}
|
||||
env:
|
||||
TOXENV: integration
|
||||
DBT_INVOCATION_ENV: github-actions
|
||||
@@ -165,6 +203,8 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: tox -- --ddtrace
|
||||
env:
|
||||
PYTEST_ADDOPTS: ${{ format('--splits {0} --group {1}', env.PYTHON_INTEGRATION_TEST_WORKERS, matrix.split-group) }}
|
||||
|
||||
- name: Get current date
|
||||
if: always()
|
||||
@@ -182,8 +222,26 @@ jobs:
|
||||
- name: Upload Integration Test Coverage to Codecov
|
||||
if: ${{ matrix.python-version == '3.11' }}
|
||||
uses: codecov/codecov-action@v3
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: integration
|
||||
|
||||
integration-report:
|
||||
if: ${{ always() }}
|
||||
name: Integration Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
needs: integration
|
||||
steps:
|
||||
- name: "Integration Tests Failed"
|
||||
if: ${{ contains(needs.integration.result, 'failure') || contains(needs.integration.result, 'cancelled') }}
|
||||
# when this is true the next step won't execute
|
||||
run: |
|
||||
echo "::notice title='Integration test suite failed'"
|
||||
exit 1
|
||||
|
||||
- name: "Integration Tests Passed"
|
||||
run: |
|
||||
echo "::notice title='Integration test suite passed'"
|
||||
|
||||
build:
|
||||
name: build packages
|
||||
|
||||
6
.github/workflows/release-docker.yml
vendored
6
.github/workflows/release-docker.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push MAJOR.MINOR.PATCH tag
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: docker/Dockerfile
|
||||
push: True
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
ghcr.io/dbt-labs/${{ github.event.inputs.package }}:${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Build and push MINOR.latest tag
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
if: ${{ needs.get_version_meta.outputs.minor_latest == 'True' }}
|
||||
with:
|
||||
file: docker/Dockerfile
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
ghcr.io/dbt-labs/${{ github.event.inputs.package }}:${{ needs.get_version_meta.outputs.major }}.${{ needs.get_version_meta.outputs.minor }}.latest
|
||||
|
||||
- name: Build and push latest tag
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
if: ${{ needs.get_version_meta.outputs.latest == 'True' }}
|
||||
with:
|
||||
file: docker/Dockerfile
|
||||
|
||||
30
.github/workflows/repository-cleanup.yml
vendored
Normal file
30
.github/workflows/repository-cleanup.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# **what?**
|
||||
# Cleanup branches left over from automation and testing. Also cleanup
|
||||
# draft releases from release testing.
|
||||
|
||||
# **why?**
|
||||
# The automations are leaving behind branches and releases that clutter
|
||||
# the repository. Sometimes we need them to debug processes so we don't
|
||||
# want them immediately deleted. Running on Saturday to avoid running
|
||||
# at the same time as an actual release to prevent breaking a release
|
||||
# mid-release.
|
||||
|
||||
# **when?**
|
||||
# Mainly on a schedule of 12:00 Saturday.
|
||||
# Manual trigger can also run on demand
|
||||
|
||||
name: Repository Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * SAT' # At 12:00 on Saturday - details in `why` above
|
||||
|
||||
workflow_dispatch: # for manual triggering
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
cleanup-repo:
|
||||
uses: dbt-labs/actions/.github/workflows/repository-cleanup.yml@main
|
||||
secrets: inherit
|
||||
@@ -18,11 +18,41 @@ on:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
# top-level adjustments can be made here
|
||||
env:
|
||||
# number of parallel processes to spawn for python testing
|
||||
PYTHON_INTEGRATION_TEST_WORKERS: 5
|
||||
|
||||
jobs:
|
||||
integration-metadata:
|
||||
name: integration test metadata generation
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
split-groups: ${{ steps.generate-split-groups.outputs.split-groups }}
|
||||
|
||||
steps:
|
||||
- name: generate split-groups
|
||||
id: generate-split-groups
|
||||
run: |
|
||||
MATRIX_JSON="["
|
||||
for B in $(seq 1 ${{ env.PYTHON_INTEGRATION_TEST_WORKERS }}); do
|
||||
MATRIX_JSON+=$(sed 's/^/"/;s/$/"/' <<< "${B}")
|
||||
done
|
||||
MATRIX_JSON="${MATRIX_JSON//\"\"/\", \"}"
|
||||
MATRIX_JSON+="]"
|
||||
echo "split-groups=${MATRIX_JSON}" >> $GITHUB_OUTPUT
|
||||
|
||||
# run the performance measurements on the current or default branch
|
||||
test-schema:
|
||||
name: Test Log Schema
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- integration-metadata
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
split-group: ${{ fromJson(needs.integration-metadata.outputs.split-groups) }}
|
||||
env:
|
||||
# turns warnings into errors
|
||||
RUSTFLAGS: "-D warnings"
|
||||
@@ -65,3 +95,14 @@ jobs:
|
||||
# we actually care if these pass, because the normal test run doesn't usually include many json log outputs
|
||||
- name: Run integration tests
|
||||
run: tox -e integration -- -nauto
|
||||
env:
|
||||
PYTEST_ADDOPTS: ${{ format('--splits {0} --group {1}', env.PYTHON_INTEGRATION_TEST_WORKERS, matrix.split-group) }}
|
||||
|
||||
test-schema-report:
|
||||
name: Log Schema Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
needs: test-schema
|
||||
steps:
|
||||
- name: "[Notification] Log test suite passes"
|
||||
run: |
|
||||
echo "::notice title="Log test suite passes""
|
||||
|
||||
@@ -37,7 +37,7 @@ repos:
|
||||
alias: flake8-check
|
||||
stages: [manual]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.3.0
|
||||
rev: v1.4.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
# N.B.: Mypy is... a bit fragile.
|
||||
|
||||
237
CHANGELOG.md
237
CHANGELOG.md
@@ -5,6 +5,243 @@
|
||||
- "Breaking changes" listed under a version may require action from end users or external maintainers when upgrading to that version.
|
||||
- Do not edit this file directly. This file is auto-generated using [changie](https://github.com/miniscruff/changie). For details on how to document a change, see [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#adding-changelog-entry)
|
||||
|
||||
## dbt-core 1.7.8 - February 14, 2024
|
||||
|
||||
### Fixes
|
||||
|
||||
- When patching versioned models, set constraints after config ([#9364](https://github.com/dbt-labs/dbt-core/issues/9364))
|
||||
- Store node_info in node associated logging events ([#9557](https://github.com/dbt-labs/dbt-core/issues/9557))
|
||||
|
||||
|
||||
|
||||
## dbt-core 1.7.7 - February 01, 2024
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix seed and source selection in `dbt docs generate` ([#9161](https://github.com/dbt-labs/dbt-core/issues/9161))
|
||||
- Add TestGenerateCatalogWithExternalNodes, include empty nodes in node selection during docs generate ([#9456](https://github.com/dbt-labs/dbt-core/issues/9456))
|
||||
|
||||
## dbt-core 1.7.6 - January 25, 2024
|
||||
|
||||
### Fixes
|
||||
|
||||
- Handle unknown `type_code` for model contracts ([#8877](https://github.com/dbt-labs/dbt-core/issues/8877), [#8353](https://github.com/dbt-labs/dbt-core/issues/8353))
|
||||
- Fix retry command run from CLI ([#9444](https://github.com/dbt-labs/dbt-core/issues/9444))
|
||||
|
||||
## dbt-core 1.7.5 - January 18, 2024
|
||||
|
||||
### Fixes
|
||||
|
||||
- Preserve the value of vars and the --full-refresh flags when using retry. ([#9112](https://github.com/dbt-labs/dbt-core/issues/9112))
|
||||
|
||||
### Contributors
|
||||
- [@peterallenwebb,](https://github.com/peterallenwebb,) ([#9112](https://github.com/dbt-labs/dbt-core/issues/9112))
|
||||
|
||||
## dbt-core 1.7.4 - December 14, 2023
|
||||
|
||||
### Features
|
||||
|
||||
- Adds support for parsing conversion metric related properties for the semantic layer. ([#9203](https://github.com/dbt-labs/dbt-core/issues/9203))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Ensure we produce valid jsonschema schemas for manifest, catalog, run-results, and sources ([#8991](https://github.com/dbt-labs/dbt-core/issues/8991))
|
||||
|
||||
### Contributors
|
||||
- [@WilliamDee](https://github.com/WilliamDee) ([#9203](https://github.com/dbt-labs/dbt-core/issues/9203))
|
||||
|
||||
## dbt-core 1.7.3 - November 29, 2023
|
||||
|
||||
### Fixes
|
||||
|
||||
- deps: Lock git packages to commit SHA during resolution ([#9050](https://github.com/dbt-labs/dbt-core/issues/9050))
|
||||
- deps: Use PackageRenderer to read package-lock.json ([#9127](https://github.com/dbt-labs/dbt-core/issues/9127))
|
||||
- Get sources working again in dbt docs generate ([#9119](https://github.com/dbt-labs/dbt-core/issues/9119))
|
||||
|
||||
## dbt-core 1.7.2 - November 16, 2023
|
||||
|
||||
### Features
|
||||
|
||||
- Support setting export configs hierarchically via saved query and project configs ([#8956](https://github.com/dbt-labs/dbt-core/issues/8956))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix formatting of tarball information in packages-lock.yml ([#9062](https://github.com/dbt-labs/dbt-core/issues/9062))
|
||||
|
||||
### Under the Hood
|
||||
|
||||
- Treat SystemExit as an interrupt if raised during node execution. ([#n/a](https://github.com/dbt-labs/dbt-core/issues/n/a))
|
||||
|
||||
### Contributors
|
||||
- [@benmosher](https://github.com/benmosher) ([#n/a](https://github.com/dbt-labs/dbt-core/issues/n/a))
|
||||
|
||||
## dbt-core 1.7.1 - November 07, 2023
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix compilation exception running empty seed file and support new Integer agate data_type ([#8895](https://github.com/dbt-labs/dbt-core/issues/8895))
|
||||
- Update run_results.json from previous versions of dbt to support deferral and rerun from failure ([#9010](https://github.com/dbt-labs/dbt-core/issues/9010))
|
||||
- Use MANIFEST.in to recursively include all jinja templates; fixes issue where some templates were not included in the distribution ([#9016](https://github.com/dbt-labs/dbt-core/issues/9016))
|
||||
- Fix git repository with subdirectory for Deps ([#9000](https://github.com/dbt-labs/dbt-core/issues/9000))
|
||||
|
||||
## dbt-core 1.7.0 - November 02, 2023
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed the FirstRunResultError and AfterFirstRunResultError event types, using the existing RunResultError in their place. ([#7963](https://github.com/dbt-labs/dbt-core/issues/7963))
|
||||
|
||||
### Features
|
||||
|
||||
- add log file of installed packages via dbt deps ([#6643](https://github.com/dbt-labs/dbt-core/issues/6643))
|
||||
- Enable re-population of metadata vars post-environment change during programmatic invocation ([#8010](https://github.com/dbt-labs/dbt-core/issues/8010))
|
||||
- Added support to configure a delimiter for a seed file, defaults to comma ([#3990](https://github.com/dbt-labs/dbt-core/issues/3990))
|
||||
- Allow specification of `create_metric: true` on measures ([#8125](https://github.com/dbt-labs/dbt-core/issues/8125))
|
||||
- Add node attributes related to compilation to run_results.json ([#7519](https://github.com/dbt-labs/dbt-core/issues/7519))
|
||||
- Add --no-inject-ephemeral-ctes flag for `compile` command, for usage by linting. ([#8480](https://github.com/dbt-labs/dbt-core/issues/8480))
|
||||
- Support configuration of semantic models with the addition of enable/disable and group enablement. ([#7968](https://github.com/dbt-labs/dbt-core/issues/7968))
|
||||
- Accept a `dbt-cloud` config in dbt_project.yml ([#8438](https://github.com/dbt-labs/dbt-core/issues/8438))
|
||||
- Support atomic replace in the global replace macro ([#8539](https://github.com/dbt-labs/dbt-core/issues/8539))
|
||||
- Use translate_type on data_type in model.columns in templates by default, remove no op `TYPE_LABELS` ([#8007](https://github.com/dbt-labs/dbt-core/issues/8007))
|
||||
- Add an option to generate static documentation ([#8614](https://github.com/dbt-labs/dbt-core/issues/8614))
|
||||
- Allow setting "access" as a config in addition to as a property ([#8383](https://github.com/dbt-labs/dbt-core/issues/8383))
|
||||
- Loosen typing requirement on renameable/replaceable relations to Iterable to allow adapters more flexibility in registering relation types, include docstrings as suggestions ([#8647](https://github.com/dbt-labs/dbt-core/issues/8647))
|
||||
- Add support for optional label in semantic_models, measures, dimensions and entities. ([#8595](https://github.com/dbt-labs/dbt-core/issues/8595), [#8755](https://github.com/dbt-labs/dbt-core/issues/8755))
|
||||
- Allow adapters to include package logs in dbt standard logging ([#7859](https://github.com/dbt-labs/dbt-core/issues/7859))
|
||||
- Support storing test failures as views ([#6914](https://github.com/dbt-labs/dbt-core/issues/6914))
|
||||
- resolve packages with same git repo and unique subdirectory ([#5374](https://github.com/dbt-labs/dbt-core/issues/5374))
|
||||
- Add new ResourceReport event to record memory/cpu/io metrics ([#8342](https://github.com/dbt-labs/dbt-core/issues/8342))
|
||||
- Adding `date_spine` macro (and supporting macros) from dbt-utils to dbt-core ([#8172](https://github.com/dbt-labs/dbt-core/issues/8172))
|
||||
- Support `fill_nulls_with` and `join_to_timespine` for metric nodes ([#8593](https://github.com/dbt-labs/dbt-core/issues/8593), [#8755](https://github.com/dbt-labs/dbt-core/issues/8755))
|
||||
- Raise a warning when a contracted model has a numeric field without scale defined ([#8183](https://github.com/dbt-labs/dbt-core/issues/8183))
|
||||
- Added support for retrieving partial catalog information from a schema ([#8521](https://github.com/dbt-labs/dbt-core/issues/8521))
|
||||
- Add meta attribute to SemanticModels config ([#8511](https://github.com/dbt-labs/dbt-core/issues/8511))
|
||||
- Selectors with docs generate limits catalog generation ([#6014](https://github.com/dbt-labs/dbt-core/issues/6014))
|
||||
- Allow freshness to be determined via DBMS metadata for supported adapters ([#8704](https://github.com/dbt-labs/dbt-core/issues/8704))
|
||||
- Add support semantic layer SavedQuery node type ([#8594](https://github.com/dbt-labs/dbt-core/issues/8594))
|
||||
- Add exports to SavedQuery spec ([#8892](https://github.com/dbt-labs/dbt-core/issues/8892))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Copy dir during `dbt deps` if symlink fails ([#7428](https://github.com/dbt-labs/dbt-core/issues/7428), [#8223](https://github.com/dbt-labs/dbt-core/issues/8223))
|
||||
- If --profile specified with dbt-init, create the project with the specified profile ([#6154](https://github.com/dbt-labs/dbt-core/issues/6154))
|
||||
- Fixed double-underline ([#5301](https://github.com/dbt-labs/dbt-core/issues/5301))
|
||||
- Copy target_schema from config into snapshot node ([#6745](https://github.com/dbt-labs/dbt-core/issues/6745))
|
||||
- Enable converting deprecation warnings to errors ([#8130](https://github.com/dbt-labs/dbt-core/issues/8130))
|
||||
- Add status to Parse Inline Error ([#8173](https://github.com/dbt-labs/dbt-core/issues/8173))
|
||||
- Ensure `warn_error_options` get serialized in `invocation_args_dict` ([#7694](https://github.com/dbt-labs/dbt-core/issues/7694))
|
||||
- Stop detecting materialization macros based on macro name ([#6231](https://github.com/dbt-labs/dbt-core/issues/6231))
|
||||
- Update `dbt deps` download retry logic to handle `EOFError` exceptions ([#6653](https://github.com/dbt-labs/dbt-core/issues/6653))
|
||||
- Improve handling of CTE injection with ephemeral models ([#8213](https://github.com/dbt-labs/dbt-core/issues/8213))
|
||||
- Fix unbound local variable error in `checked_agg_time_dimension_for_measure` ([#8230](https://github.com/dbt-labs/dbt-core/issues/8230))
|
||||
- Ensure runtime errors are raised for graph runnable tasks (compile, show, run, etc) ([#8166](https://github.com/dbt-labs/dbt-core/issues/8166))
|
||||
- Fix retry not working with log-file-max-bytes ([#8297](https://github.com/dbt-labs/dbt-core/issues/8297))
|
||||
- Add explicit support for integers for the show command ([#8153](https://github.com/dbt-labs/dbt-core/issues/8153))
|
||||
- Detect changes to model access, version, or latest_version in state:modified ([#8189](https://github.com/dbt-labs/dbt-core/issues/8189))
|
||||
- Add connection status into list of statuses for dbt debug ([#8350](https://github.com/dbt-labs/dbt-core/issues/8350))
|
||||
- fix fqn-selection for external versioned models ([#8374](https://github.com/dbt-labs/dbt-core/issues/8374))
|
||||
- Fix: DbtInternalError after model that previously ref'd external model is deleted ([#8375](https://github.com/dbt-labs/dbt-core/issues/8375))
|
||||
- Fix using list command with path selector and project-dir ([#8385](https://github.com/dbt-labs/dbt-core/issues/8385))
|
||||
- Remedy performance regression by only writing run_results.json once. ([#8360](https://github.com/dbt-labs/dbt-core/issues/8360))
|
||||
- Add support for swapping materialized views with tables/views and vice versa ([#8449](https://github.com/dbt-labs/dbt-core/issues/8449))
|
||||
- Turn breaking changes to contracted models into warnings for unversioned models ([#8384](https://github.com/dbt-labs/dbt-core/issues/8384), [#8282](https://github.com/dbt-labs/dbt-core/issues/8282))
|
||||
- Ensure parsing does not break when `window_groupings` is not specified for `non_additive_dimension` ([#8453](https://github.com/dbt-labs/dbt-core/issues/8453))
|
||||
- fix ambiguous reference error for tests and versions when model name is duplicated across packages ([#8327](https://github.com/dbt-labs/dbt-core/issues/8327), [#8493](https://github.com/dbt-labs/dbt-core/issues/8493))
|
||||
- Fix "Internal Error: Expected node <unique-id> not found in manifest" when depends_on set on ModelNodeArgs ([#8506](https://github.com/dbt-labs/dbt-core/issues/8506))
|
||||
- Fix snapshot success message ([#7583](https://github.com/dbt-labs/dbt-core/issues/7583))
|
||||
- Parse the correct schema version from manifest ([#8544](https://github.com/dbt-labs/dbt-core/issues/8544))
|
||||
- make version comparison insensitive to order ([#8571](https://github.com/dbt-labs/dbt-core/issues/8571))
|
||||
- Update metric helper functions to work with new semantic layer metrics ([#8134](https://github.com/dbt-labs/dbt-core/issues/8134))
|
||||
- Disallow cleaning paths outside current working directory ([#8318](https://github.com/dbt-labs/dbt-core/issues/8318))
|
||||
- Warn when --state == --target ([#8160](https://github.com/dbt-labs/dbt-core/issues/8160))
|
||||
- update dbt show to include limit in DWH query ([#8496,](https://github.com/dbt-labs/dbt-core/issues/8496,), [#8417](https://github.com/dbt-labs/dbt-core/issues/8417))
|
||||
- Support quoted parameter list for MultiOption CLI options. ([#8598](https://github.com/dbt-labs/dbt-core/issues/8598))
|
||||
- Support global flags passed in after subcommands ([#6497](https://github.com/dbt-labs/dbt-core/issues/6497))
|
||||
- Lower bound of `8.0.2` for `click` ([#8683](https://github.com/dbt-labs/dbt-core/issues/8683))
|
||||
- Fixes test type edges filter ([#8692](https://github.com/dbt-labs/dbt-core/issues/8692))
|
||||
- semantic models in graph selection ([#8589](https://github.com/dbt-labs/dbt-core/issues/8589))
|
||||
- Support doc blocks in nested semantic model YAML ([#8509](https://github.com/dbt-labs/dbt-core/issues/8509))
|
||||
- avoid double-rendering sql_header in dbt show ([#8739](https://github.com/dbt-labs/dbt-core/issues/8739))
|
||||
- Fix tag selection for projects with semantic models ([#8749](https://github.com/dbt-labs/dbt-core/issues/8749))
|
||||
- Foreign key constraint on incremental model results in Database Error ([#8022](https://github.com/dbt-labs/dbt-core/issues/8022))
|
||||
- Support docs blocks on versioned model column descriptions ([#8540](https://github.com/dbt-labs/dbt-core/issues/8540))
|
||||
- Enable seeds to be handled from stored manifest data ([#6875](https://github.com/dbt-labs/dbt-core/issues/6875))
|
||||
- Override path-like args in dbt retry ([#8682](https://github.com/dbt-labs/dbt-core/issues/8682))
|
||||
- Group updates on unmodified nodes are handled gracefully for state:modified ([#8371](https://github.com/dbt-labs/dbt-core/issues/8371))
|
||||
- Partial parsing fix for adding groups and updating models at the same time ([#8697](https://github.com/dbt-labs/dbt-core/issues/8697))
|
||||
- Fix partial parsing not working for semantic model change ([#8859](https://github.com/dbt-labs/dbt-core/issues/8859))
|
||||
- Rework get_catalog implementation to retain previous adapter interface semantics ([#8846](https://github.com/dbt-labs/dbt-core/issues/8846))
|
||||
- Add back contract enforcement for temporary tables on postgres ([#8857](https://github.com/dbt-labs/dbt-core/issues/8857))
|
||||
- Add version to fqn when version==0 ([#8836](https://github.com/dbt-labs/dbt-core/issues/8836))
|
||||
- Fix cased comparison in catalog-retrieval function. ([#8939](https://github.com/dbt-labs/dbt-core/issues/8939))
|
||||
- Catalog queries now assign the correct type to materialized views ([#8864](https://github.com/dbt-labs/dbt-core/issues/8864))
|
||||
- Make relation filtering None-tolerant for maximal flexibility across adapters. ([#8974](https://github.com/dbt-labs/dbt-core/issues/8974))
|
||||
|
||||
### Docs
|
||||
|
||||
- Corrected spelling of "Partiton" ([dbt-docs/#8100](https://github.com/dbt-labs/dbt-docs/issues/8100))
|
||||
- Remove static SQL codeblock for metrics ([dbt-docs/#436](https://github.com/dbt-labs/dbt-docs/issues/436))
|
||||
- fixed comment util.py ([dbt-docs/#None](https://github.com/dbt-labs/dbt-docs/issues/None))
|
||||
- Fix newline escapes and improve formatting in docker README ([dbt-docs/#8211](https://github.com/dbt-labs/dbt-docs/issues/8211))
|
||||
- Display contract and column constraints on the model page ([dbt-docs/#433](https://github.com/dbt-labs/dbt-docs/issues/433))
|
||||
- Display semantic model details in docs ([dbt-docs/#431](https://github.com/dbt-labs/dbt-docs/issues/431))
|
||||
|
||||
### Under the Hood
|
||||
|
||||
- Switch from hologram to mashumaro jsonschema ([#8426](https://github.com/dbt-labs/dbt-core/issues/8426))
|
||||
- Refactor flaky test pp_versioned_models ([#7781](https://github.com/dbt-labs/dbt-core/issues/7781))
|
||||
- format exception from dbtPlugin.initialize ([#8152](https://github.com/dbt-labs/dbt-core/issues/8152))
|
||||
- A way to control maxBytes for a single dbt.log file ([#8199](https://github.com/dbt-labs/dbt-core/issues/8199))
|
||||
- Ref expressions with version can now be processed by the latest version of the high-performance dbt-extractor library. ([#7688](https://github.com/dbt-labs/dbt-core/issues/7688))
|
||||
- Bump manifest schema version to v11, freeze manifest v10 ([#8333](https://github.com/dbt-labs/dbt-core/issues/8333))
|
||||
- add tracking for plugin.get_nodes calls ([#8344](https://github.com/dbt-labs/dbt-core/issues/8344))
|
||||
- add internal flag: --no-partial-parse-file-diff to inform whether to compute a file diff during partial parsing ([#8363](https://github.com/dbt-labs/dbt-core/issues/8363))
|
||||
- Add return values to a number of functions for mypy ([#8389](https://github.com/dbt-labs/dbt-core/issues/8389))
|
||||
- Fix mypy warnings for ManifestLoader.load() ([#8401](https://github.com/dbt-labs/dbt-core/issues/8401))
|
||||
- Use python version 3.10.7 in Docker image. ([#8444](https://github.com/dbt-labs/dbt-core/issues/8444))
|
||||
- Re-organize jinja macros: relation-specific in /macros/adapters/relations/<relation>, relation agnostic in /macros/relations ([#8449](https://github.com/dbt-labs/dbt-core/issues/8449))
|
||||
- Update typing to meet mypy standards ([#8396](https://github.com/dbt-labs/dbt-core/issues/8396))
|
||||
- Mypy errors - adapters/factory.py ([#8387](https://github.com/dbt-labs/dbt-core/issues/8387))
|
||||
- Added more type annotations. ([#8537](https://github.com/dbt-labs/dbt-core/issues/8537))
|
||||
- Audit potential circular dependencies ([#8349](https://github.com/dbt-labs/dbt-core/issues/8349))
|
||||
- Add functional test for advanced ref override ([#8566](https://github.com/dbt-labs/dbt-core/issues/8566))
|
||||
- Add typing to __init__ in base.py ([#8398](https://github.com/dbt-labs/dbt-core/issues/8398))
|
||||
- Fix untyped functions in task/runnable.py (mypy warning) ([#8402](https://github.com/dbt-labs/dbt-core/issues/8402))
|
||||
- add a test for ephemeral cte injection ([#8225](https://github.com/dbt-labs/dbt-core/issues/8225))
|
||||
- Fix test_numeric_values to look for more specific strings ([#8470](https://github.com/dbt-labs/dbt-core/issues/8470))
|
||||
- Pin types-requests<2.31.0 in `dev-requirements.txt` ([#8789](https://github.com/dbt-labs/dbt-core/issues/8789))
|
||||
- Add warning_tag to UnversionedBreakingChange ([#8827](https://github.com/dbt-labs/dbt-core/issues/8827))
|
||||
- Update v10 manifest schema to match 1.6 for testing schema compatibility ([#8835](https://github.com/dbt-labs/dbt-core/issues/8835))
|
||||
- Add a no-op runner for Saved Qeury ([#8893](https://github.com/dbt-labs/dbt-core/issues/8893))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Bump mypy from 1.3.0 to 1.4.0 ([#7912](https://github.com/dbt-labs/dbt-core/pull/7912))
|
||||
- Bump mypy from 1.4.0 to 1.4.1 ([#8219](https://github.com/dbt-labs/dbt-core/pull/8219))
|
||||
- Update pin for click<9 ([#8232](https://github.com/dbt-labs/dbt-core/pull/8232))
|
||||
- Add upper bound to sqlparse pin of <0.5 ([#8236](https://github.com/dbt-labs/dbt-core/pull/8236))
|
||||
- Support dbt-semantic-interfaces 0.2.0 ([#8250](https://github.com/dbt-labs/dbt-core/pull/8250))
|
||||
- Bump docker/build-push-action from 4 to 5 ([#8783](https://github.com/dbt-labs/dbt-core/pull/8783))
|
||||
- Upgrade dbt-semantic-interfaces dep to 0.3.0 ([#8819](https://github.com/dbt-labs/dbt-core/pull/8819))
|
||||
- Begin using DSI 0.4.x ([#8892](https://github.com/dbt-labs/dbt-core/pull/8892))
|
||||
|
||||
### Contributors
|
||||
- [@anjutiwari](https://github.com/anjutiwari) ([#7428](https://github.com/dbt-labs/dbt-core/issues/7428), [#8223](https://github.com/dbt-labs/dbt-core/issues/8223))
|
||||
- [@benmosher](https://github.com/benmosher) ([#8480](https://github.com/dbt-labs/dbt-core/issues/8480))
|
||||
- [@d-kaneshiro](https://github.com/d-kaneshiro) ([#None](https://github.com/dbt-labs/dbt-core/issues/None))
|
||||
- [@dave-connors-3](https://github.com/dave-connors-3) ([#8153](https://github.com/dbt-labs/dbt-core/issues/8153), [#8589](https://github.com/dbt-labs/dbt-core/issues/8589))
|
||||
- [@dylan-murray](https://github.com/dylan-murray) ([#8683](https://github.com/dbt-labs/dbt-core/issues/8683))
|
||||
- [@ezraerb](https://github.com/ezraerb) ([#6154](https://github.com/dbt-labs/dbt-core/issues/6154))
|
||||
- [@gem7318](https://github.com/gem7318) ([#8010](https://github.com/dbt-labs/dbt-core/issues/8010))
|
||||
- [@jamezrin](https://github.com/jamezrin) ([#8211](https://github.com/dbt-labs/dbt-core/issues/8211))
|
||||
- [@jusbaldw](https://github.com/jusbaldw) ([#6643](https://github.com/dbt-labs/dbt-core/issues/6643))
|
||||
- [@lllong33](https://github.com/lllong33) ([#5301](https://github.com/dbt-labs/dbt-core/issues/5301))
|
||||
- [@marcodamore](https://github.com/marcodamore) ([#436](https://github.com/dbt-labs/dbt-core/issues/436))
|
||||
- [@mescanne](https://github.com/mescanne) ([#8614](https://github.com/dbt-labs/dbt-core/issues/8614))
|
||||
- [@pgoslatara](https://github.com/pgoslatara) ([#8100](https://github.com/dbt-labs/dbt-core/issues/8100))
|
||||
- [@philippeboyd](https://github.com/philippeboyd) ([#5374](https://github.com/dbt-labs/dbt-core/issues/5374))
|
||||
- [@ramonvermeulen](https://github.com/ramonvermeulen) ([#3990](https://github.com/dbt-labs/dbt-core/issues/3990))
|
||||
- [@renanleme](https://github.com/renanleme) ([#8692](https://github.com/dbt-labs/dbt-core/issues/8692))
|
||||
|
||||
## Previous Releases
|
||||
|
||||
For information on prior major and minor releases, see their changelogs:
|
||||
|
||||
13
codecov.yml
13
codecov.yml
@@ -0,0 +1,13 @@
|
||||
ignore:
|
||||
- ".github"
|
||||
- ".changes"
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.1% # Reduce noise by ignoring rounding errors in coverage drops
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 80%
|
||||
|
||||
@@ -7,12 +7,12 @@ from dbt.exceptions import DbtRuntimeError
|
||||
|
||||
@dataclass
|
||||
class Column:
|
||||
# Note: This is automatically used by contract code
|
||||
# No-op conversions (INTEGER => INT) have been removed.
|
||||
# Any adapter that wants to take advantage of "translate_type"
|
||||
# should create a ClassVar with the appropriate conversions.
|
||||
TYPE_LABELS: ClassVar[Dict[str, str]] = {
|
||||
"STRING": "TEXT",
|
||||
"TIMESTAMP": "TIMESTAMP",
|
||||
"FLOAT": "FLOAT",
|
||||
"INTEGER": "INT",
|
||||
"BOOLEAN": "BOOLEAN",
|
||||
}
|
||||
column: str
|
||||
dtype: str
|
||||
|
||||
@@ -72,7 +72,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
|
||||
TYPE: str = NotImplemented
|
||||
|
||||
def __init__(self, profile: AdapterRequiredConfig):
|
||||
def __init__(self, profile: AdapterRequiredConfig) -> None:
|
||||
self.profile = profile
|
||||
self.thread_connections: Dict[Hashable, Connection] = {}
|
||||
self.lock: RLock = flags.MP_CONTEXT.RLock()
|
||||
@@ -400,7 +400,7 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
def execute(
|
||||
self, sql: str, auto_begin: bool = False, fetch: bool = False
|
||||
self, sql: str, auto_begin: bool = False, fetch: bool = False, limit: Optional[int] = None
|
||||
) -> Tuple[AdapterResponse, agate.Table]:
|
||||
"""Execute the given SQL.
|
||||
|
||||
@@ -408,7 +408,28 @@ class BaseConnectionManager(metaclass=abc.ABCMeta):
|
||||
:param bool auto_begin: If set, and dbt is not currently inside a
|
||||
transaction, automatically begin one.
|
||||
:param bool fetch: If set, fetch results.
|
||||
:param int limit: If set, limits the result set
|
||||
:return: A tuple of the query status and results (empty if fetch=False).
|
||||
:rtype: Tuple[AdapterResponse, agate.Table]
|
||||
"""
|
||||
raise dbt.exceptions.NotImplementedError("`execute` is not implemented for this adapter!")
|
||||
|
||||
def add_select_query(self, sql: str) -> Tuple[Connection, Any]:
|
||||
"""
|
||||
This was added here because base.impl.BaseAdapter.get_column_schema_from_query expects it to be here.
|
||||
That method wouldn't work unless the adapter used sql.impl.SQLAdapter, sql.connections.SQLConnectionManager
|
||||
or defined this method on <Adapter>ConnectionManager before passing it in to <Adapter>Adapter.
|
||||
|
||||
See https://github.com/dbt-labs/dbt-core/issues/8396 for more information.
|
||||
"""
|
||||
raise dbt.exceptions.NotImplementedError(
|
||||
"`add_select_query` is not implemented for this adapter!"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def data_type_code_to_name(cls, type_code: Union[int, str]) -> str:
|
||||
"""Get the string representation of the data type from the type_code."""
|
||||
# https://peps.python.org/pep-0249/#type-objects
|
||||
raise dbt.exceptions.NotImplementedError(
|
||||
"`data_type_code_to_name` is not implemented for this adapter!"
|
||||
)
|
||||
|
||||
@@ -17,9 +17,11 @@ from typing import (
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dbt.adapters.capability import Capability, CapabilityDict
|
||||
from dbt.contracts.graph.nodes import ColumnLevelConstraint, ConstraintType, ModelLevelConstraint
|
||||
|
||||
import agate
|
||||
@@ -43,8 +45,14 @@ from dbt.exceptions import (
|
||||
UnexpectedNullError,
|
||||
)
|
||||
|
||||
from dbt.adapters.protocol import AdapterConfig, ConnectionManagerProtocol
|
||||
from dbt.clients.agate_helper import empty_table, merge_tables, table_from_rows
|
||||
from dbt.adapters.protocol import AdapterConfig
|
||||
from dbt.clients.agate_helper import (
|
||||
empty_table,
|
||||
get_column_value_uncased,
|
||||
merge_tables,
|
||||
table_from_rows,
|
||||
Integer,
|
||||
)
|
||||
from dbt.clients.jinja import MacroGenerator
|
||||
from dbt.contracts.graph.manifest import Manifest, MacroManifest
|
||||
from dbt.contracts.graph.nodes import ResultNode
|
||||
@@ -60,7 +68,7 @@ from dbt.events.types import (
|
||||
)
|
||||
from dbt.utils import filter_null_values, executor, cast_to_str, AttrDict
|
||||
|
||||
from dbt.adapters.base.connections import Connection, AdapterResponse
|
||||
from dbt.adapters.base.connections import Connection, AdapterResponse, BaseConnectionManager
|
||||
from dbt.adapters.base.meta import AdapterMeta, available
|
||||
from dbt.adapters.base.relation import (
|
||||
ComponentName,
|
||||
@@ -74,7 +82,9 @@ from dbt.adapters.cache import RelationsCache, _make_ref_key_dict
|
||||
from dbt import deprecations
|
||||
|
||||
GET_CATALOG_MACRO_NAME = "get_catalog"
|
||||
GET_CATALOG_RELATIONS_MACRO_NAME = "get_catalog_relations"
|
||||
FRESHNESS_MACRO_NAME = "collect_freshness"
|
||||
GET_RELATION_LAST_MODIFIED_MACRO_NAME = "get_relation_last_modified"
|
||||
|
||||
|
||||
class ConstraintSupport(str, Enum):
|
||||
@@ -109,7 +119,7 @@ def _catalog_filter_schemas(manifest: Manifest) -> Callable[[agate.Row], bool]:
|
||||
return test
|
||||
|
||||
|
||||
def _utc(dt: Optional[datetime], source: BaseRelation, field_name: str) -> datetime:
|
||||
def _utc(dt: Optional[datetime], source: Optional[BaseRelation], field_name: str) -> datetime:
|
||||
"""If dt has a timezone, return a new datetime that's in UTC. Otherwise,
|
||||
assume the datetime is already for UTC and add the timezone.
|
||||
"""
|
||||
@@ -161,6 +171,12 @@ class PythonJobHelper:
|
||||
raise NotImplementedError("PythonJobHelper submit function is not implemented yet")
|
||||
|
||||
|
||||
class FreshnessResponse(TypedDict):
|
||||
max_loaded_at: datetime
|
||||
snapshotted_at: datetime
|
||||
age: float # age in seconds
|
||||
|
||||
|
||||
class BaseAdapter(metaclass=AdapterMeta):
|
||||
"""The BaseAdapter provides an abstract base class for adapters.
|
||||
|
||||
@@ -208,7 +224,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
|
||||
Relation: Type[BaseRelation] = BaseRelation
|
||||
Column: Type[BaseColumn] = BaseColumn
|
||||
ConnectionManager: Type[ConnectionManagerProtocol]
|
||||
ConnectionManager: Type[BaseConnectionManager]
|
||||
|
||||
# A set of clobber config fields accepted by this adapter
|
||||
# for use in materializations
|
||||
@@ -222,7 +238,11 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
ConstraintType.foreign_key: ConstraintSupport.ENFORCED,
|
||||
}
|
||||
|
||||
def __init__(self, config):
|
||||
# This static member variable can be overriden in concrete adapter
|
||||
# implementations to indicate adapter support for optional capabilities.
|
||||
_capabilities = CapabilityDict({})
|
||||
|
||||
def __init__(self, config) -> None:
|
||||
self.config = config
|
||||
self.cache = RelationsCache()
|
||||
self.connections = self.ConnectionManager(config)
|
||||
@@ -315,14 +335,21 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
|
||||
@available.parse(lambda *a, **k: ("", empty_table()))
|
||||
def get_partitions_metadata(self, table: str) -> Tuple[agate.Table]:
|
||||
"""Obtain partitions metadata for a BigQuery partitioned table.
|
||||
"""
|
||||
TODO: Can we move this to dbt-bigquery?
|
||||
Obtain partitions metadata for a BigQuery partitioned table.
|
||||
|
||||
:param str table_id: a partitioned table id, in standard SQL format.
|
||||
:param str table: a partitioned table id, in standard SQL format.
|
||||
:return: a partition metadata tuple, as described in
|
||||
https://cloud.google.com/bigquery/docs/creating-partitioned-tables#getting_partition_metadata_using_meta_tables.
|
||||
:rtype: agate.Table
|
||||
"""
|
||||
return self.connections.get_partitions_metadata(table=table)
|
||||
if hasattr(self.connections, "get_partitions_metadata"):
|
||||
return self.connections.get_partitions_metadata(table=table)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"`get_partitions_metadata` is not implemented for this adapter!"
|
||||
)
|
||||
|
||||
###
|
||||
# Methods that should never be overridden
|
||||
@@ -408,7 +435,30 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
lowercase strings.
|
||||
"""
|
||||
info_schema_name_map = SchemaSearchMap()
|
||||
nodes: Iterator[ResultNode] = chain(
|
||||
relations = self._get_catalog_relations(manifest)
|
||||
for relation in relations:
|
||||
info_schema_name_map.add(relation)
|
||||
# result is a map whose keys are information_schema Relations without
|
||||
# identifiers that have appropriate database prefixes, and whose values
|
||||
# are sets of lowercase schema names that are valid members of those
|
||||
# databases
|
||||
return info_schema_name_map
|
||||
|
||||
def _get_catalog_relations_by_info_schema(
|
||||
self, relations
|
||||
) -> Dict[InformationSchema, List[BaseRelation]]:
|
||||
relations_by_info_schema: Dict[InformationSchema, List[BaseRelation]] = dict()
|
||||
for relation in relations:
|
||||
info_schema = relation.information_schema_only()
|
||||
if info_schema not in relations_by_info_schema:
|
||||
relations_by_info_schema[info_schema] = []
|
||||
relations_by_info_schema[info_schema].append(relation)
|
||||
|
||||
return relations_by_info_schema
|
||||
|
||||
def _get_catalog_relations(self, manifest: Manifest) -> List[BaseRelation]:
|
||||
|
||||
nodes = chain(
|
||||
[
|
||||
node
|
||||
for node in manifest.nodes.values()
|
||||
@@ -416,14 +466,9 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
],
|
||||
manifest.sources.values(),
|
||||
)
|
||||
for node in nodes:
|
||||
relation = self.Relation.create_from(self.config, node)
|
||||
info_schema_name_map.add(relation)
|
||||
# result is a map whose keys are information_schema Relations without
|
||||
# identifiers that have appropriate database prefixes, and whose values
|
||||
# are sets of lowercase schema names that are valid members of those
|
||||
# databases
|
||||
return info_schema_name_map
|
||||
|
||||
relations = [self.Relation.create_from(self.config, n) for n in nodes]
|
||||
return relations
|
||||
|
||||
def _relations_cache_for_schemas(
|
||||
self, manifest: Manifest, cache_schemas: Optional[Set[BaseRelation]] = None
|
||||
@@ -453,9 +498,10 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
# it's possible that there were no relations in some schemas. We want
|
||||
# to insert the schemas we query into the cache's `.schemas` attribute
|
||||
# so we can check it later
|
||||
cache_update: Set[Tuple[Optional[str], Optional[str]]] = set()
|
||||
cache_update: Set[Tuple[Optional[str], str]] = set()
|
||||
for relation in cache_schemas:
|
||||
cache_update.add((relation.database, relation.schema))
|
||||
if relation.schema:
|
||||
cache_update.add((relation.database, relation.schema))
|
||||
self.cache.update_schemas(cache_update)
|
||||
|
||||
def set_relations_cache(
|
||||
@@ -917,6 +963,17 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
"""
|
||||
raise NotImplementedError("`convert_number_type` is not implemented for this adapter!")
|
||||
|
||||
@classmethod
|
||||
def convert_integer_type(cls, agate_table: agate.Table, col_idx: int) -> str:
|
||||
"""Return the type in the database that best maps to the agate.Number
|
||||
type for the given agate table and column index.
|
||||
|
||||
:param agate_table: The table
|
||||
:param col_idx: The index into the agate table for the column.
|
||||
:return: The name of the type in the database
|
||||
"""
|
||||
return "integer"
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def convert_boolean_type(cls, agate_table: agate.Table, col_idx: int) -> str:
|
||||
@@ -974,6 +1031,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
def convert_agate_type(cls, agate_table: agate.Table, col_idx: int) -> Optional[str]:
|
||||
agate_type: Type = agate_table.column_types[col_idx]
|
||||
conversions: List[Tuple[Type, Callable[..., str]]] = [
|
||||
(Integer, cls.convert_integer_type),
|
||||
(agate.Text, cls.convert_text_type),
|
||||
(agate.Number, cls.convert_number_type),
|
||||
(agate.Boolean, cls.convert_boolean_type),
|
||||
@@ -1085,25 +1143,108 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
results = self._catalog_filter_table(table, manifest) # type: ignore[arg-type]
|
||||
return results
|
||||
|
||||
def get_catalog(self, manifest: Manifest) -> Tuple[agate.Table, List[Exception]]:
|
||||
schema_map = self._get_catalog_schemas(manifest)
|
||||
def _get_one_catalog_by_relations(
|
||||
self,
|
||||
information_schema: InformationSchema,
|
||||
relations: List[BaseRelation],
|
||||
manifest: Manifest,
|
||||
) -> agate.Table:
|
||||
|
||||
kwargs = {
|
||||
"information_schema": information_schema,
|
||||
"relations": relations,
|
||||
}
|
||||
table = self.execute_macro(
|
||||
GET_CATALOG_RELATIONS_MACRO_NAME,
|
||||
kwargs=kwargs,
|
||||
# pass in the full manifest, so we get any local project
|
||||
# overrides
|
||||
manifest=manifest,
|
||||
)
|
||||
|
||||
results = self._catalog_filter_table(table, manifest) # type: ignore[arg-type]
|
||||
return results
|
||||
|
||||
def get_filtered_catalog(
|
||||
self, manifest: Manifest, relations: Optional[Set[BaseRelation]] = None
|
||||
):
|
||||
catalogs: agate.Table
|
||||
if (
|
||||
relations is None
|
||||
or len(relations) > 100
|
||||
or not self.supports(Capability.SchemaMetadataByRelations)
|
||||
):
|
||||
# Do it the traditional way. We get the full catalog.
|
||||
catalogs, exceptions = self.get_catalog(manifest)
|
||||
else:
|
||||
# Do it the new way. We try to save time by selecting information
|
||||
# only for the exact set of relations we are interested in.
|
||||
catalogs, exceptions = self.get_catalog_by_relations(manifest, relations)
|
||||
|
||||
if relations and catalogs:
|
||||
relation_map = {
|
||||
(
|
||||
r.database.casefold() if r.database else None,
|
||||
r.schema.casefold() if r.schema else None,
|
||||
r.identifier.casefold() if r.identifier else None,
|
||||
)
|
||||
for r in relations
|
||||
}
|
||||
|
||||
def in_map(row: agate.Row):
|
||||
d = _expect_row_value("table_database", row)
|
||||
s = _expect_row_value("table_schema", row)
|
||||
i = _expect_row_value("table_name", row)
|
||||
d = d.casefold() if d is not None else None
|
||||
s = s.casefold() if s is not None else None
|
||||
i = i.casefold() if i is not None else None
|
||||
return (d, s, i) in relation_map
|
||||
|
||||
catalogs = catalogs.where(in_map)
|
||||
|
||||
return catalogs, exceptions
|
||||
|
||||
def row_matches_relation(self, row: agate.Row, relations: Set[BaseRelation]):
|
||||
pass
|
||||
|
||||
def get_catalog(self, manifest: Manifest) -> Tuple[agate.Table, List[Exception]]:
|
||||
with executor(self.config) as tpe:
|
||||
futures: List[Future[agate.Table]] = []
|
||||
schema_map: SchemaSearchMap = self._get_catalog_schemas(manifest)
|
||||
for info, schemas in schema_map.items():
|
||||
if len(schemas) == 0:
|
||||
continue
|
||||
name = ".".join([str(info.database), "information_schema"])
|
||||
|
||||
fut = tpe.submit_connected(
|
||||
self, name, self._get_one_catalog, info, schemas, manifest
|
||||
)
|
||||
futures.append(fut)
|
||||
|
||||
catalogs, exceptions = catch_as_completed(futures)
|
||||
|
||||
catalogs, exceptions = catch_as_completed(futures)
|
||||
return catalogs, exceptions
|
||||
|
||||
def get_catalog_by_relations(
|
||||
self, manifest: Manifest, relations: Set[BaseRelation]
|
||||
) -> Tuple[agate.Table, List[Exception]]:
|
||||
with executor(self.config) as tpe:
|
||||
futures: List[Future[agate.Table]] = []
|
||||
relations_by_schema = self._get_catalog_relations_by_info_schema(relations)
|
||||
for info_schema in relations_by_schema:
|
||||
name = ".".join([str(info_schema.database), "information_schema"])
|
||||
relations = set(relations_by_schema[info_schema])
|
||||
fut = tpe.submit_connected(
|
||||
self,
|
||||
name,
|
||||
self._get_one_catalog_by_relations,
|
||||
info_schema,
|
||||
relations,
|
||||
manifest,
|
||||
)
|
||||
futures.append(fut)
|
||||
|
||||
catalogs, exceptions = catch_as_completed(futures)
|
||||
return catalogs, exceptions
|
||||
|
||||
def cancel_open_connections(self):
|
||||
"""Cancel all open connections."""
|
||||
return self.connections.cancel_open()
|
||||
@@ -1114,7 +1255,7 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
loaded_at_field: str,
|
||||
filter: Optional[str],
|
||||
manifest: Optional[Manifest] = None,
|
||||
) -> Tuple[Optional[AdapterResponse], Dict[str, Any]]:
|
||||
) -> Tuple[Optional[AdapterResponse], FreshnessResponse]:
|
||||
"""Calculate the freshness of sources in dbt, and return it"""
|
||||
kwargs: Dict[str, Any] = {
|
||||
"source": source,
|
||||
@@ -1149,13 +1290,52 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
|
||||
snapshotted_at = _utc(table[0][1], source, loaded_at_field)
|
||||
age = (snapshotted_at - max_loaded_at).total_seconds()
|
||||
freshness = {
|
||||
freshness: FreshnessResponse = {
|
||||
"max_loaded_at": max_loaded_at,
|
||||
"snapshotted_at": snapshotted_at,
|
||||
"age": age,
|
||||
}
|
||||
return adapter_response, freshness
|
||||
|
||||
def calculate_freshness_from_metadata(
|
||||
self,
|
||||
source: BaseRelation,
|
||||
manifest: Optional[Manifest] = None,
|
||||
) -> Tuple[Optional[AdapterResponse], FreshnessResponse]:
|
||||
kwargs: Dict[str, Any] = {
|
||||
"information_schema": source.information_schema_only(),
|
||||
"relations": [source],
|
||||
}
|
||||
result = self.execute_macro(
|
||||
GET_RELATION_LAST_MODIFIED_MACRO_NAME, kwargs=kwargs, manifest=manifest
|
||||
)
|
||||
adapter_response, table = result.response, result.table # type: ignore[attr-defined]
|
||||
|
||||
try:
|
||||
row = table[0]
|
||||
last_modified_val = get_column_value_uncased("last_modified", row)
|
||||
snapshotted_at_val = get_column_value_uncased("snapshotted_at", row)
|
||||
except Exception:
|
||||
raise MacroResultError(GET_RELATION_LAST_MODIFIED_MACRO_NAME, table)
|
||||
|
||||
if last_modified_val is None:
|
||||
# Interpret missing value as "infinitely long ago"
|
||||
max_loaded_at = datetime(1, 1, 1, 0, 0, 0, tzinfo=pytz.UTC)
|
||||
else:
|
||||
max_loaded_at = _utc(last_modified_val, None, "last_modified")
|
||||
|
||||
snapshotted_at = _utc(snapshotted_at_val, None, "snapshotted_at")
|
||||
|
||||
age = (snapshotted_at - max_loaded_at).total_seconds()
|
||||
|
||||
freshness: FreshnessResponse = {
|
||||
"max_loaded_at": max_loaded_at,
|
||||
"snapshotted_at": snapshotted_at,
|
||||
"age": age,
|
||||
}
|
||||
|
||||
return adapter_response, freshness
|
||||
|
||||
def pre_model_hook(self, config: Mapping[str, Any]) -> Any:
|
||||
"""A hook for running some operation before the model materialization
|
||||
runs. The hook can assume it has a connection available.
|
||||
@@ -1429,6 +1609,14 @@ class BaseAdapter(metaclass=AdapterMeta):
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def capabilities(cls) -> CapabilityDict:
|
||||
return cls._capabilities
|
||||
|
||||
@classmethod
|
||||
def supports(cls, capability: Capability) -> bool:
|
||||
return bool(cls.capabilities()[capability])
|
||||
|
||||
|
||||
COLUMNS_EQUAL_SQL = """
|
||||
with diff_count as (
|
||||
|
||||
@@ -93,7 +93,7 @@ class AdapterMeta(abc.ABCMeta):
|
||||
_available_: FrozenSet[str]
|
||||
_parse_replacements_: Dict[str, Callable]
|
||||
|
||||
def __new__(mcls, name, bases, namespace, **kwargs):
|
||||
def __new__(mcls, name, bases, namespace, **kwargs) -> "AdapterMeta":
|
||||
# mypy does not like the `**kwargs`. But `ABCMeta` itself takes
|
||||
# `**kwargs` in its argspec here (and passes them to `type.__new__`.
|
||||
# I'm not sure there is any benefit to it after poking around a bit,
|
||||
|
||||
@@ -29,7 +29,7 @@ class AdapterPlugin:
|
||||
credentials: Type[Credentials],
|
||||
include_path: str,
|
||||
dependencies: Optional[List[str]] = None,
|
||||
):
|
||||
) -> None:
|
||||
|
||||
self.adapter: Type[AdapterProtocol] = adapter
|
||||
self.credentials: Type[Credentials] = credentials
|
||||
|
||||
@@ -11,7 +11,7 @@ from dbt.exceptions import DbtRuntimeError
|
||||
|
||||
|
||||
class NodeWrapper:
|
||||
def __init__(self, node):
|
||||
def __init__(self, node) -> None:
|
||||
self._inner_node = node
|
||||
|
||||
def __getattr__(self, name):
|
||||
@@ -25,9 +25,9 @@ class _QueryComment(local):
|
||||
- a source_name indicating what set the current thread's query comment
|
||||
"""
|
||||
|
||||
def __init__(self, initial):
|
||||
def __init__(self, initial) -> None:
|
||||
self.query_comment: Optional[str] = initial
|
||||
self.append = False
|
||||
self.append: bool = False
|
||||
|
||||
def add(self, sql: str) -> str:
|
||||
if not self.query_comment:
|
||||
@@ -57,7 +57,7 @@ QueryStringFunc = Callable[[str, Optional[NodeWrapper]], str]
|
||||
|
||||
|
||||
class MacroQueryStringSetter:
|
||||
def __init__(self, config: AdapterRequiredConfig, manifest: Manifest):
|
||||
def __init__(self, config: AdapterRequiredConfig, manifest: Manifest) -> None:
|
||||
self.manifest = manifest
|
||||
self.config = config
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from collections.abc import Hashable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, TypeVar, Any, Type, Dict, Iterator, Tuple, Set
|
||||
from typing import Optional, TypeVar, Any, Type, Dict, Iterator, Tuple, Set, Union, FrozenSet
|
||||
|
||||
from dbt.contracts.graph.nodes import SourceDefinition, ManifestNode, ResultNode, ParsedNode
|
||||
from dbt.contracts.relation import (
|
||||
@@ -23,6 +23,7 @@ import dbt.exceptions
|
||||
|
||||
|
||||
Self = TypeVar("Self", bound="BaseRelation")
|
||||
SerializableIterable = Union[Tuple, FrozenSet]
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False, repr=False)
|
||||
@@ -36,6 +37,18 @@ class BaseRelation(FakeAPIObject, Hashable):
|
||||
quote_policy: Policy = field(default_factory=lambda: Policy())
|
||||
dbt_created: bool = False
|
||||
|
||||
# register relation types that can be renamed for the purpose of replacing relations using stages and backups
|
||||
# adding a relation type here also requires defining the associated rename macro
|
||||
# e.g. adding RelationType.View in dbt-postgres requires that you define:
|
||||
# include/postgres/macros/relations/view/rename.sql::postgres__get_rename_view_sql()
|
||||
renameable_relations: SerializableIterable = ()
|
||||
|
||||
# register relation types that are atomically replaceable, e.g. they have "create or replace" syntax
|
||||
# adding a relation type here also requires defining the associated replace macro
|
||||
# e.g. adding RelationType.View in dbt-postgres requires that you define:
|
||||
# include/postgres/macros/relations/view/replace.sql::postgres__get_replace_view_sql()
|
||||
replaceable_relations: SerializableIterable = ()
|
||||
|
||||
def _is_exactish_match(self, field: ComponentName, value: str) -> bool:
|
||||
if self.dbt_created and self.quote_policy.get_part(field) is False:
|
||||
return self.path.get_lowered_part(field) == value.lower()
|
||||
@@ -169,7 +182,6 @@ class BaseRelation(FakeAPIObject, Hashable):
|
||||
return self.include(identifier=False).replace_path(identifier=None)
|
||||
|
||||
def _render_iterator(self) -> Iterator[Tuple[Optional[ComponentName], Optional[str]]]:
|
||||
|
||||
for key in ComponentName:
|
||||
path_part: Optional[str] = None
|
||||
if self.include_policy.get_part(key):
|
||||
@@ -286,6 +298,14 @@ class BaseRelation(FakeAPIObject, Hashable):
|
||||
)
|
||||
return cls.from_dict(kwargs)
|
||||
|
||||
@property
|
||||
def can_be_renamed(self) -> bool:
|
||||
return self.type in self.renameable_relations
|
||||
|
||||
@property
|
||||
def can_be_replaced(self) -> bool:
|
||||
return self.type in self.replaceable_relations
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<{} {}>".format(self.__class__.__name__, self.render())
|
||||
|
||||
@@ -439,11 +459,11 @@ class SchemaSearchMap(Dict[InformationSchema, Set[Optional[str]]]):
|
||||
self[key].add(schema)
|
||||
|
||||
def search(self) -> Iterator[Tuple[InformationSchema, Optional[str]]]:
|
||||
for information_schema_name, schemas in self.items():
|
||||
for information_schema, schemas in self.items():
|
||||
for schema in schemas:
|
||||
yield information_schema_name, schema
|
||||
yield information_schema, schema
|
||||
|
||||
def flatten(self, allow_multiple_databases: bool = False):
|
||||
def flatten(self, allow_multiple_databases: bool = False) -> "SchemaSearchMap":
|
||||
new = self.__class__()
|
||||
|
||||
# make sure we don't have multiple databases if allow_multiple_databases is set to False
|
||||
|
||||
@@ -38,8 +38,8 @@ class _CachedRelation:
|
||||
:attr BaseRelation inner: The underlying dbt relation.
|
||||
"""
|
||||
|
||||
def __init__(self, inner):
|
||||
self.referenced_by = {}
|
||||
def __init__(self, inner) -> None:
|
||||
self.referenced_by: Dict[_ReferenceKey, _CachedRelation] = {}
|
||||
self.inner = inner
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
52
core/dbt/adapters/capability.py
Normal file
52
core/dbt/adapters/capability.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional, DefaultDict, Mapping
|
||||
|
||||
|
||||
class Capability(str, Enum):
|
||||
"""Enumeration of optional adapter features which can be probed using BaseAdapter.has_feature()"""
|
||||
|
||||
SchemaMetadataByRelations = "SchemaMetadataByRelations"
|
||||
"""Indicates efficient support for retrieving schema metadata for a list of relations, rather than always retrieving
|
||||
all the relations in a schema."""
|
||||
|
||||
TableLastModifiedMetadata = "TableLastModifiedMetadata"
|
||||
"""Indicates support for determining the time of the last table modification by querying database metadata."""
|
||||
|
||||
|
||||
class Support(str, Enum):
|
||||
Unknown = "Unknown"
|
||||
"""The adapter has not declared whether this capability is a feature of the underlying DBMS."""
|
||||
|
||||
Unsupported = "Unsupported"
|
||||
"""This capability is not possible with the underlying DBMS, so the adapter does not implement related macros."""
|
||||
|
||||
NotImplemented = "NotImplemented"
|
||||
"""This capability is available in the underlying DBMS, but support has not yet been implemented in the adapter."""
|
||||
|
||||
Versioned = "Versioned"
|
||||
"""Some versions of the DBMS supported by the adapter support this capability and the adapter has implemented any
|
||||
macros needed to use it."""
|
||||
|
||||
Full = "Full"
|
||||
"""All versions of the DBMS supported by the adapter support this capability and the adapter has implemented any
|
||||
macros needed to use it."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CapabilitySupport:
|
||||
support: Support
|
||||
first_version: Optional[str] = None
|
||||
|
||||
def __bool__(self):
|
||||
return self.support == Support.Versioned or self.support == Support.Full
|
||||
|
||||
|
||||
class CapabilityDict(DefaultDict[Capability, CapabilitySupport]):
|
||||
def __init__(self, vals: Mapping[Capability, CapabilitySupport]):
|
||||
super().__init__(self._default)
|
||||
self.update(vals)
|
||||
|
||||
@staticmethod
|
||||
def _default():
|
||||
return CapabilitySupport(support=Support.Unknown)
|
||||
@@ -19,7 +19,7 @@ Adapter = AdapterProtocol
|
||||
|
||||
|
||||
class AdapterContainer:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.lock = threading.Lock()
|
||||
self.adapters: Dict[str, Adapter] = {}
|
||||
self.plugins: Dict[str, AdapterPlugin] = {}
|
||||
|
||||
@@ -90,7 +90,7 @@ class AdapterProtocol( # type: ignore[misc]
|
||||
ConnectionManager: Type[ConnectionManager_T]
|
||||
connections: ConnectionManager_T
|
||||
|
||||
def __init__(self, config: AdapterRequiredConfig):
|
||||
def __init__(self, config: AdapterRequiredConfig) -> None:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import abc
|
||||
import time
|
||||
from typing import List, Optional, Tuple, Any, Iterable, Dict, Union
|
||||
from typing import List, Optional, Tuple, Any, Iterable, Dict
|
||||
|
||||
import agate
|
||||
|
||||
@@ -131,14 +131,6 @@ class SQLConnectionManager(BaseConnectionManager):
|
||||
|
||||
return dbt.clients.agate_helper.table_from_data_flat(data, column_names)
|
||||
|
||||
@classmethod
|
||||
def data_type_code_to_name(cls, type_code: Union[int, str]) -> str:
|
||||
"""Get the string representation of the data type from the type_code."""
|
||||
# https://peps.python.org/pep-0249/#type-objects
|
||||
raise dbt.exceptions.NotImplementedError(
|
||||
"`data_type_code_to_name` is not implemented for this adapter!"
|
||||
)
|
||||
|
||||
def execute(
|
||||
self, sql: str, auto_begin: bool = False, fetch: bool = False, limit: Optional[int] = None
|
||||
) -> Tuple[AdapterResponse, agate.Table]:
|
||||
|
||||
@@ -75,6 +75,10 @@ class SQLAdapter(BaseAdapter):
|
||||
decimals = agate_table.aggregate(agate.MaxPrecision(col_idx)) # type: ignore[attr-defined]
|
||||
return "float8" if decimals else "integer"
|
||||
|
||||
@classmethod
|
||||
def convert_integer_type(cls, agate_table: agate.Table, col_idx: int) -> str:
|
||||
return "integer"
|
||||
|
||||
@classmethod
|
||||
def convert_boolean_type(cls, agate_table: agate.Table, col_idx: int) -> str:
|
||||
return "boolean"
|
||||
|
||||
@@ -24,6 +24,7 @@ if os.name != "nt":
|
||||
FLAGS_DEFAULTS = {
|
||||
"INDIRECT_SELECTION": "eager",
|
||||
"TARGET_PATH": None,
|
||||
"WARN_ERROR": None,
|
||||
# Cli args without user_config or env var option.
|
||||
"FULL_REFRESH": False,
|
||||
"STRICT_MODE": False,
|
||||
@@ -57,11 +58,10 @@ def args_to_context(args: List[str]) -> Context:
|
||||
from dbt.cli.main import cli
|
||||
|
||||
cli_ctx = cli.make_context(cli.name, args)
|
||||
# Split args if they're a comma seperated string.
|
||||
# Split args if they're a comma separated string.
|
||||
if len(args) == 1 and "," in args[0]:
|
||||
args = args[0].split(",")
|
||||
sub_command_name, sub_command, args = cli.resolve_command(cli_ctx, args)
|
||||
|
||||
# Handle source and docs group.
|
||||
if isinstance(sub_command, Group):
|
||||
sub_command_name, sub_command, args = sub_command.resolve_command(cli_ctx, args)
|
||||
@@ -79,7 +79,6 @@ class Flags:
|
||||
def __init__(
|
||||
self, ctx: Optional[Context] = None, user_config: Optional[UserConfig] = None
|
||||
) -> None:
|
||||
|
||||
# Set the default flags.
|
||||
for key, value in FLAGS_DEFAULTS.items():
|
||||
object.__setattr__(self, key, value)
|
||||
@@ -121,7 +120,6 @@ class Flags:
|
||||
# respected over DBT_PRINT or --print.
|
||||
new_name: Union[str, None] = None
|
||||
if param_name in DEPRECATED_PARAMS:
|
||||
|
||||
# Deprecated env vars can only be set via env var.
|
||||
# We use the deprecated option in click to serialize the value
|
||||
# from the env var string.
|
||||
@@ -316,10 +314,8 @@ def command_params(command: CliCommand, args_dict: Dict[str, Any]) -> CommandPar
|
||||
default_args = set([x.lower() for x in FLAGS_DEFAULTS.keys()])
|
||||
|
||||
res = command.to_list()
|
||||
|
||||
for k, v in args_dict.items():
|
||||
k = k.lower()
|
||||
|
||||
# if a "which" value exists in the args dict, it should match the command provided
|
||||
if k == WHICH_KEY:
|
||||
if v != command.value:
|
||||
@@ -329,7 +325,9 @@ def command_params(command: CliCommand, args_dict: Dict[str, Any]) -> CommandPar
|
||||
continue
|
||||
|
||||
# param was assigned from defaults and should not be included
|
||||
if k not in (cmd_args | prnt_args) - default_args:
|
||||
if k not in (cmd_args | prnt_args) or (
|
||||
k in default_args and v == FLAGS_DEFAULTS[k.upper()]
|
||||
):
|
||||
continue
|
||||
|
||||
# if the param is in parent args, it should come before the arg name
|
||||
@@ -342,9 +340,14 @@ def command_params(command: CliCommand, args_dict: Dict[str, Any]) -> CommandPar
|
||||
|
||||
spinal_cased = k.replace("_", "-")
|
||||
|
||||
# MultiOption flags come back as lists, but we want to pass them as space separated strings
|
||||
if isinstance(v, list):
|
||||
v = " ".join(v)
|
||||
|
||||
if k == "macro" and command == CliCommand.RUN_OPERATION:
|
||||
add_fn(v)
|
||||
elif v in (None, False):
|
||||
# None is a Singleton, False is a Flyweight, only one instance of each.
|
||||
elif v is None or v is False:
|
||||
add_fn(f"--no-{spinal_cased}")
|
||||
elif v is True:
|
||||
add_fn(f"--{spinal_cased}")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import functools
|
||||
from copy import copy
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, List, Optional, Union
|
||||
@@ -64,7 +65,7 @@ class dbtRunner:
|
||||
self,
|
||||
manifest: Optional[Manifest] = None,
|
||||
callbacks: Optional[List[Callable[[EventMsg], None]]] = None,
|
||||
):
|
||||
) -> None:
|
||||
self.manifest = manifest
|
||||
|
||||
if callbacks is None:
|
||||
@@ -118,6 +119,44 @@ class dbtRunner:
|
||||
)
|
||||
|
||||
|
||||
# approach from https://github.com/pallets/click/issues/108#issuecomment-280489786
|
||||
def global_flags(func):
|
||||
@p.cache_selected_only
|
||||
@p.debug
|
||||
@p.deprecated_print
|
||||
@p.enable_legacy_logger
|
||||
@p.fail_fast
|
||||
@p.log_cache_events
|
||||
@p.log_file_max_bytes
|
||||
@p.log_format_file
|
||||
@p.log_level
|
||||
@p.log_level_file
|
||||
@p.log_path
|
||||
@p.macro_debugging
|
||||
@p.partial_parse
|
||||
@p.partial_parse_file_path
|
||||
@p.partial_parse_file_diff
|
||||
@p.populate_cache
|
||||
@p.print
|
||||
@p.printer_width
|
||||
@p.quiet
|
||||
@p.record_timing_info
|
||||
@p.send_anonymous_usage_stats
|
||||
@p.single_threaded
|
||||
@p.static_parser
|
||||
@p.use_colors
|
||||
@p.use_colors_file
|
||||
@p.use_experimental_parser
|
||||
@p.version
|
||||
@p.version_check
|
||||
@p.write_json
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# dbt
|
||||
@click.group(
|
||||
context_settings={"help_option_names": ["-h", "--help"]},
|
||||
@@ -126,36 +165,11 @@ class dbtRunner:
|
||||
epilog="Specify one of these sub-commands and you can find more help from there.",
|
||||
)
|
||||
@click.pass_context
|
||||
@p.cache_selected_only
|
||||
@p.debug
|
||||
@p.deprecated_print
|
||||
@p.enable_legacy_logger
|
||||
@p.fail_fast
|
||||
@p.log_cache_events
|
||||
@p.log_format
|
||||
@p.log_format_file
|
||||
@p.log_level
|
||||
@p.log_level_file
|
||||
@p.log_path
|
||||
@p.macro_debugging
|
||||
@p.partial_parse
|
||||
@p.partial_parse_file_path
|
||||
@p.populate_cache
|
||||
@p.print
|
||||
@p.printer_width
|
||||
@p.quiet
|
||||
@p.record_timing_info
|
||||
@p.send_anonymous_usage_stats
|
||||
@p.single_threaded
|
||||
@p.static_parser
|
||||
@p.use_colors
|
||||
@p.use_colors_file
|
||||
@p.use_experimental_parser
|
||||
@p.version
|
||||
@p.version_check
|
||||
@global_flags
|
||||
@p.warn_error
|
||||
@p.warn_error_options
|
||||
@p.write_json
|
||||
@p.log_format
|
||||
@p.show_resource_report
|
||||
def cli(ctx, **kwargs):
|
||||
"""An ELT tool for managing your SQL transformations and data models.
|
||||
For more documentation on these commands, visit: docs.getdbt.com
|
||||
@@ -165,13 +179,14 @@ def cli(ctx, **kwargs):
|
||||
# dbt build
|
||||
@cli.command("build")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.defer
|
||||
@p.deprecated_defer
|
||||
@p.exclude
|
||||
@p.fail_fast
|
||||
@p.favor_state
|
||||
@p.deprecated_favor_state
|
||||
@p.full_refresh
|
||||
@p.include_saved_query
|
||||
@p.indirect_selection
|
||||
@p.profile
|
||||
@p.profiles_dir
|
||||
@@ -188,7 +203,6 @@ def cli(ctx, **kwargs):
|
||||
@p.target_path
|
||||
@p.threads
|
||||
@p.vars
|
||||
@p.version_check
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
@requires.profile
|
||||
@@ -211,6 +225,8 @@ def build(ctx, **kwargs):
|
||||
# dbt clean
|
||||
@cli.command("clean")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.clean_project_files_only
|
||||
@p.profile
|
||||
@p.profiles_dir
|
||||
@p.project_dir
|
||||
@@ -233,6 +249,7 @@ def clean(ctx, **kwargs):
|
||||
# dbt docs
|
||||
@cli.group()
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
def docs(ctx, **kwargs):
|
||||
"""Generate or serve the documentation website for your project"""
|
||||
|
||||
@@ -240,6 +257,7 @@ def docs(ctx, **kwargs):
|
||||
# dbt docs generate
|
||||
@docs.command("generate")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.compile_docs
|
||||
@p.defer
|
||||
@p.deprecated_defer
|
||||
@@ -252,6 +270,7 @@ def docs(ctx, **kwargs):
|
||||
@p.select
|
||||
@p.selector
|
||||
@p.empty_catalog
|
||||
@p.static
|
||||
@p.state
|
||||
@p.defer_state
|
||||
@p.deprecated_state
|
||||
@@ -259,7 +278,6 @@ def docs(ctx, **kwargs):
|
||||
@p.target_path
|
||||
@p.threads
|
||||
@p.vars
|
||||
@p.version_check
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
@requires.profile
|
||||
@@ -282,6 +300,7 @@ def docs_generate(ctx, **kwargs):
|
||||
# dbt docs serve
|
||||
@docs.command("serve")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.browser
|
||||
@p.port
|
||||
@p.profile
|
||||
@@ -310,6 +329,7 @@ def docs_serve(ctx, **kwargs):
|
||||
# dbt compile
|
||||
@cli.command("compile")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.defer
|
||||
@p.deprecated_defer
|
||||
@p.exclude
|
||||
@@ -328,11 +348,11 @@ def docs_serve(ctx, **kwargs):
|
||||
@p.state
|
||||
@p.defer_state
|
||||
@p.deprecated_state
|
||||
@p.compile_inject_ephemeral_ctes
|
||||
@p.target
|
||||
@p.target_path
|
||||
@p.threads
|
||||
@p.vars
|
||||
@p.version_check
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
@requires.profile
|
||||
@@ -356,6 +376,7 @@ def compile(ctx, **kwargs):
|
||||
# dbt show
|
||||
@cli.command("show")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.defer
|
||||
@p.deprecated_defer
|
||||
@p.exclude
|
||||
@@ -379,7 +400,6 @@ def compile(ctx, **kwargs):
|
||||
@p.target_path
|
||||
@p.threads
|
||||
@p.vars
|
||||
@p.version_check
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
@requires.profile
|
||||
@@ -403,6 +423,7 @@ def show(ctx, **kwargs):
|
||||
# dbt debug
|
||||
@cli.command("debug")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.debug_connection
|
||||
@p.config_dir
|
||||
@p.profile
|
||||
@@ -410,7 +431,6 @@ def show(ctx, **kwargs):
|
||||
@p.project_dir
|
||||
@p.target
|
||||
@p.vars
|
||||
@p.version_check
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
def debug(ctx, **kwargs):
|
||||
@@ -429,18 +449,46 @@ def debug(ctx, **kwargs):
|
||||
# dbt deps
|
||||
@cli.command("deps")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.profile
|
||||
@p.profiles_dir_exists_false
|
||||
@p.project_dir
|
||||
@p.target
|
||||
@p.vars
|
||||
@p.source
|
||||
@p.dry_run
|
||||
@p.lock
|
||||
@p.upgrade
|
||||
@p.add_package
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
@requires.unset_profile
|
||||
@requires.project
|
||||
def deps(ctx, **kwargs):
|
||||
"""Pull the most recent version of the dependencies listed in packages.yml"""
|
||||
task = DepsTask(ctx.obj["flags"], ctx.obj["project"])
|
||||
"""Install dbt packages specified.
|
||||
In the following case, a new `package-lock.yml` will be generated and the packages are installed:
|
||||
- user updated the packages.yml
|
||||
- user specify the flag --update, which means for packages that are specified as a
|
||||
range, dbt-core will try to install the newer version
|
||||
Otherwise, deps will use `package-lock.yml` as source of truth to install packages.
|
||||
|
||||
There is a way to add new packages by providing an `--add-package` flag to deps command
|
||||
which will allow user to specify a package they want to add in the format of packagename@version.
|
||||
"""
|
||||
flags = ctx.obj["flags"]
|
||||
if flags.ADD_PACKAGE:
|
||||
if not flags.ADD_PACKAGE["version"] and flags.SOURCE != "local":
|
||||
raise BadOptionUsage(
|
||||
message=f"Version is required in --add-package when a package when source is {flags.SOURCE}",
|
||||
option_name="--add-package",
|
||||
)
|
||||
else:
|
||||
if flags.DRY_RUN:
|
||||
raise BadOptionUsage(
|
||||
message="Invalid flag `--dry-run` when not using `--add-package`.",
|
||||
option_name="--dry-run",
|
||||
)
|
||||
task = DepsTask(flags, ctx.obj["project"])
|
||||
results = task.run()
|
||||
success = task.interpret_results(results)
|
||||
return results, success
|
||||
@@ -449,6 +497,7 @@ def deps(ctx, **kwargs):
|
||||
# dbt init
|
||||
@cli.command("init")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
# for backwards compatibility, accept 'project_name' as an optional positional argument
|
||||
@click.argument("project_name", required=False)
|
||||
@p.profile
|
||||
@@ -471,6 +520,7 @@ def init(ctx, **kwargs):
|
||||
# dbt list
|
||||
@cli.command("list")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.exclude
|
||||
@p.indirect_selection
|
||||
@p.models
|
||||
@@ -516,6 +566,7 @@ cli.add_command(ls, "ls")
|
||||
# dbt parse
|
||||
@cli.command("parse")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.profile
|
||||
@p.profiles_dir
|
||||
@p.project_dir
|
||||
@@ -523,7 +574,6 @@ cli.add_command(ls, "ls")
|
||||
@p.target_path
|
||||
@p.threads
|
||||
@p.vars
|
||||
@p.version_check
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
@requires.profile
|
||||
@@ -533,19 +583,18 @@ cli.add_command(ls, "ls")
|
||||
def parse(ctx, **kwargs):
|
||||
"""Parses the project and provides information on performance"""
|
||||
# manifest generation and writing happens in @requires.manifest
|
||||
|
||||
return ctx.obj["manifest"], True
|
||||
|
||||
|
||||
# dbt run
|
||||
@cli.command("run")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.defer
|
||||
@p.deprecated_defer
|
||||
@p.favor_state
|
||||
@p.deprecated_favor_state
|
||||
@p.exclude
|
||||
@p.fail_fast
|
||||
@p.full_refresh
|
||||
@p.profile
|
||||
@p.profiles_dir
|
||||
@@ -559,7 +608,6 @@ def parse(ctx, **kwargs):
|
||||
@p.target_path
|
||||
@p.threads
|
||||
@p.vars
|
||||
@p.version_check
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
@requires.profile
|
||||
@@ -582,6 +630,7 @@ def run(ctx, **kwargs):
|
||||
# dbt retry
|
||||
@cli.command("retry")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.project_dir
|
||||
@p.profiles_dir
|
||||
@p.vars
|
||||
@@ -589,19 +638,18 @@ def run(ctx, **kwargs):
|
||||
@p.target
|
||||
@p.state
|
||||
@p.threads
|
||||
@p.fail_fast
|
||||
@p.full_refresh
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
@requires.profile
|
||||
@requires.project
|
||||
@requires.runtime_config
|
||||
@requires.manifest
|
||||
def retry(ctx, **kwargs):
|
||||
"""Retry the nodes that failed in the previous run."""
|
||||
# Retry will parse manifest inside the task after we consolidate the flags
|
||||
task = RetryTask(
|
||||
ctx.obj["flags"],
|
||||
ctx.obj["runtime_config"],
|
||||
ctx.obj["manifest"],
|
||||
)
|
||||
|
||||
results = task.run()
|
||||
@@ -612,6 +660,7 @@ def retry(ctx, **kwargs):
|
||||
# dbt clone
|
||||
@cli.command("clone")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.defer_state
|
||||
@p.exclude
|
||||
@p.full_refresh
|
||||
@@ -626,7 +675,6 @@ def retry(ctx, **kwargs):
|
||||
@p.target_path
|
||||
@p.threads
|
||||
@p.vars
|
||||
@p.version_check
|
||||
@requires.preflight
|
||||
@requires.profile
|
||||
@requires.project
|
||||
@@ -649,6 +697,7 @@ def clone(ctx, **kwargs):
|
||||
# dbt run operation
|
||||
@cli.command("run-operation")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@click.argument("macro")
|
||||
@p.args
|
||||
@p.profile
|
||||
@@ -680,6 +729,7 @@ def run_operation(ctx, **kwargs):
|
||||
# dbt seed
|
||||
@cli.command("seed")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.exclude
|
||||
@p.full_refresh
|
||||
@p.profile
|
||||
@@ -695,7 +745,6 @@ def run_operation(ctx, **kwargs):
|
||||
@p.target_path
|
||||
@p.threads
|
||||
@p.vars
|
||||
@p.version_check
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
@requires.profile
|
||||
@@ -717,6 +766,7 @@ def seed(ctx, **kwargs):
|
||||
# dbt snapshot
|
||||
@cli.command("snapshot")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.defer
|
||||
@p.deprecated_defer
|
||||
@p.exclude
|
||||
@@ -756,6 +806,7 @@ def snapshot(ctx, **kwargs):
|
||||
# dbt source
|
||||
@cli.group()
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
def source(ctx, **kwargs):
|
||||
"""Manage your project's sources"""
|
||||
|
||||
@@ -763,6 +814,7 @@ def source(ctx, **kwargs):
|
||||
# dbt source freshness
|
||||
@source.command("freshness")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.exclude
|
||||
@p.output_path # TODO: Is this ok to re-use? We have three different output params, how much can we consolidate?
|
||||
@p.profile
|
||||
@@ -805,10 +857,10 @@ cli.commands["source"].add_command(snapshot_freshness, "snapshot-freshness") #
|
||||
# dbt test
|
||||
@cli.command("test")
|
||||
@click.pass_context
|
||||
@global_flags
|
||||
@p.defer
|
||||
@p.deprecated_defer
|
||||
@p.exclude
|
||||
@p.fail_fast
|
||||
@p.favor_state
|
||||
@p.deprecated_favor_state
|
||||
@p.indirect_selection
|
||||
@@ -825,7 +877,6 @@ cli.commands["source"].add_command(snapshot_freshness, "snapshot-freshness") #
|
||||
@p.target_path
|
||||
@p.threads
|
||||
@p.vars
|
||||
@p.version_check
|
||||
@requires.postflight
|
||||
@requires.preflight
|
||||
@requires.profile
|
||||
|
||||
@@ -22,6 +22,26 @@ class YAML(ParamType):
|
||||
self.fail(f"String '{value}' is not valid YAML", param, ctx)
|
||||
|
||||
|
||||
class Package(ParamType):
|
||||
"""The Click STRING type. Converts string into dict with package name and version.
|
||||
Example package:
|
||||
package-name@1.0.0
|
||||
package-name
|
||||
"""
|
||||
|
||||
name = "NewPackage"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
# assume non-string values are a problem
|
||||
if not isinstance(value, str):
|
||||
self.fail(f"Cannot load Package from type {type(value)}", param, ctx)
|
||||
try:
|
||||
package_name, package_version = value.split("@")
|
||||
return {"name": package_name, "version": package_version}
|
||||
except ValueError:
|
||||
return {"name": value, "version": None}
|
||||
|
||||
|
||||
class WarnErrorOptionsType(YAML):
|
||||
"""The Click WarnErrorOptions type. Converts YAML strings into objects."""
|
||||
|
||||
|
||||
@@ -2,19 +2,21 @@ import click
|
||||
import inspect
|
||||
import typing as t
|
||||
from click import Context
|
||||
from click.parser import OptionParser, ParsingState
|
||||
from dbt.cli.option_types import ChoiceTuple
|
||||
|
||||
|
||||
# Implementation from: https://stackoverflow.com/a/48394004
|
||||
# Note MultiOption options must be specified with type=tuple or type=ChoiceTuple (https://github.com/pallets/click/issues/2012)
|
||||
class MultiOption(click.Option):
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self.save_other_options = kwargs.pop("save_other_options", True)
|
||||
nargs = kwargs.pop("nargs", -1)
|
||||
assert nargs == -1, "nargs, if set, must be -1 not {}".format(nargs)
|
||||
super(MultiOption, self).__init__(*args, **kwargs)
|
||||
self._previous_parser_process = None
|
||||
self._eat_all_parser = None
|
||||
# this makes mypy happy, setting these to None causes mypy failures
|
||||
self._previous_parser_process = lambda *args, **kwargs: None
|
||||
self._eat_all_parser = lambda *args, **kwargs: None
|
||||
|
||||
# validate that multiple=True
|
||||
multiple = kwargs.pop("multiple", None)
|
||||
@@ -29,34 +31,35 @@ class MultiOption(click.Option):
|
||||
else:
|
||||
assert isinstance(option_type, ChoiceTuple), msg
|
||||
|
||||
def add_to_parser(self, parser, ctx):
|
||||
def parser_process(value, state):
|
||||
def add_to_parser(self, parser: OptionParser, ctx: Context):
|
||||
def parser_process(value: str, state: ParsingState):
|
||||
# method to hook to the parser.process
|
||||
done = False
|
||||
value = [value]
|
||||
value_list = str.split(value, " ")
|
||||
if self.save_other_options:
|
||||
# grab everything up to the next option
|
||||
while state.rargs and not done:
|
||||
for prefix in self._eat_all_parser.prefixes:
|
||||
for prefix in self._eat_all_parser.prefixes: # type: ignore[attr-defined]
|
||||
if state.rargs[0].startswith(prefix):
|
||||
done = True
|
||||
if not done:
|
||||
value.append(state.rargs.pop(0))
|
||||
value_list.append(state.rargs.pop(0))
|
||||
else:
|
||||
# grab everything remaining
|
||||
value += state.rargs
|
||||
value_list += state.rargs
|
||||
state.rargs[:] = []
|
||||
value = tuple(value)
|
||||
value_tuple = tuple(value_list)
|
||||
# call the actual process
|
||||
self._previous_parser_process(value, state)
|
||||
self._previous_parser_process(value_tuple, state)
|
||||
|
||||
retval = super(MultiOption, self).add_to_parser(parser, ctx)
|
||||
for name in self.opts:
|
||||
our_parser = parser._long_opt.get(name) or parser._short_opt.get(name)
|
||||
if our_parser:
|
||||
self._eat_all_parser = our_parser
|
||||
self._eat_all_parser = our_parser # type: ignore[assignment]
|
||||
self._previous_parser_process = our_parser.process
|
||||
our_parser.process = parser_process
|
||||
# mypy doesnt like assingment to a method see https://github.com/python/mypy/issues/708
|
||||
our_parser.process = parser_process # type: ignore[method-assign]
|
||||
break
|
||||
return retval
|
||||
|
||||
|
||||
@@ -2,10 +2,16 @@ from pathlib import Path
|
||||
|
||||
import click
|
||||
from dbt.cli.options import MultiOption
|
||||
from dbt.cli.option_types import YAML, ChoiceTuple, WarnErrorOptionsType
|
||||
from dbt.cli.option_types import YAML, ChoiceTuple, WarnErrorOptionsType, Package
|
||||
from dbt.cli.resolvers import default_project_dir, default_profiles_dir
|
||||
from dbt.version import get_version_information
|
||||
|
||||
add_package = click.option(
|
||||
"--add-package",
|
||||
help="Add a package to current package spec, specify it as package-name@version. Change the source with --source flag.",
|
||||
envvar=None,
|
||||
type=Package(),
|
||||
)
|
||||
args = click.option(
|
||||
"--args",
|
||||
envvar=None,
|
||||
@@ -40,6 +46,14 @@ compile_docs = click.option(
|
||||
default=True,
|
||||
)
|
||||
|
||||
compile_inject_ephemeral_ctes = click.option(
|
||||
"--inject-ephemeral-ctes/--no-inject-ephemeral-ctes",
|
||||
envvar=None,
|
||||
help="Internal flag controlling injection of referenced ephemeral models' CTEs during `compile`.",
|
||||
hidden=True,
|
||||
default=True,
|
||||
)
|
||||
|
||||
config_dir = click.option(
|
||||
"--config-dir",
|
||||
envvar=None,
|
||||
@@ -69,6 +83,14 @@ deprecated_defer = click.option(
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
dry_run = click.option(
|
||||
"--dry-run",
|
||||
envvar=None,
|
||||
help="Option to run `dbt deps --add-package` without updating package-lock.yml file.",
|
||||
is_flag=True,
|
||||
)
|
||||
|
||||
|
||||
enable_legacy_logger = click.option(
|
||||
"--enable-legacy-logger/--no-enable-legacy-logger",
|
||||
envvar="DBT_ENABLE_LEGACY_LOGGER",
|
||||
@@ -119,6 +141,13 @@ indirect_selection = click.option(
|
||||
default="eager",
|
||||
)
|
||||
|
||||
lock = click.option(
|
||||
"--lock",
|
||||
envvar=None,
|
||||
help="Generate the package-lock.yml file without install the packages.",
|
||||
is_flag=True,
|
||||
)
|
||||
|
||||
log_cache_events = click.option(
|
||||
"--log-cache-events/--no-log-cache-events",
|
||||
help="Enable verbose logging for relational cache events to help when debugging.",
|
||||
@@ -171,6 +200,15 @@ use_colors_file = click.option(
|
||||
default=True,
|
||||
)
|
||||
|
||||
log_file_max_bytes = click.option(
|
||||
"--log-file-max-bytes",
|
||||
envvar="DBT_LOG_FILE_MAX_BYTES",
|
||||
help="Configure the max file size in bytes for a single dbt.log file, before rolling over. 0 means no limit.",
|
||||
default=10 * 1024 * 1024, # 10mb
|
||||
type=click.INT,
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
log_path = click.option(
|
||||
"--log-path",
|
||||
envvar="DBT_LOG_PATH",
|
||||
@@ -248,6 +286,14 @@ partial_parse_file_path = click.option(
|
||||
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
||||
)
|
||||
|
||||
partial_parse_file_diff = click.option(
|
||||
"--partial-parse-file-diff/--no-partial-parse-file-diff",
|
||||
envvar="DBT_PARTIAL_PARSE_FILE_DIFF",
|
||||
help="Internal flag for whether to compute a file diff during partial parsing.",
|
||||
hidden=True,
|
||||
default=True,
|
||||
)
|
||||
|
||||
populate_cache = click.option(
|
||||
"--populate-cache/--no-populate-cache",
|
||||
envvar="DBT_POPULATE_CACHE",
|
||||
@@ -290,7 +336,7 @@ printer_width = click.option(
|
||||
profile = click.option(
|
||||
"--profile",
|
||||
envvar=None,
|
||||
help="Which profile to load. Overrides setting in dbt_project.yml.",
|
||||
help="Which existing profile to load. Overrides setting in dbt_project.yml.",
|
||||
)
|
||||
|
||||
profiles_dir = click.option(
|
||||
@@ -343,6 +389,7 @@ resource_type = click.option(
|
||||
type=ChoiceTuple(
|
||||
[
|
||||
"metric",
|
||||
"semantic_model",
|
||||
"source",
|
||||
"analysis",
|
||||
"model",
|
||||
@@ -360,6 +407,14 @@ resource_type = click.option(
|
||||
default=(),
|
||||
)
|
||||
|
||||
include_saved_query = click.option(
|
||||
"--include-saved-query/--no-include-saved-query",
|
||||
envvar="DBT_INCLUDE_SAVED_QUERY",
|
||||
help="Include saved queries in the list of resources to be selected for build command",
|
||||
is_flag=True,
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
model_decls = ("-m", "--models", "--model")
|
||||
select_decls = ("-s", "--select")
|
||||
select_attrs = {
|
||||
@@ -380,9 +435,9 @@ inline = click.option(
|
||||
# Most CLI arguments should use the combined `select` option that aliases `--models` to `--select`.
|
||||
# However, if you need to split out these separators (like `dbt ls`), use the `models` and `raw_select` options instead.
|
||||
# See https://github.com/dbt-labs/dbt-core/pull/6774#issuecomment-1408476095 for more info.
|
||||
models = click.option(*model_decls, **select_attrs)
|
||||
raw_select = click.option(*select_decls, **select_attrs)
|
||||
select = click.option(*select_decls, *model_decls, **select_attrs)
|
||||
models = click.option(*model_decls, **select_attrs) # type: ignore[arg-type]
|
||||
raw_select = click.option(*select_decls, **select_attrs) # type: ignore[arg-type]
|
||||
select = click.option(*select_decls, *model_decls, **select_attrs) # type: ignore[arg-type]
|
||||
|
||||
selector = click.option(
|
||||
"--selector",
|
||||
@@ -397,6 +452,13 @@ send_anonymous_usage_stats = click.option(
|
||||
default=True,
|
||||
)
|
||||
|
||||
clean_project_files_only = click.option(
|
||||
"--clean-project-files-only / --no-clean-project-files-only",
|
||||
envvar="DBT_CLEAN_PROJECT_FILES_ONLY",
|
||||
help="If disabled, dbt clean will delete all paths specified in clean-paths, even if they're outside the dbt project.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
show = click.option(
|
||||
"--show",
|
||||
envvar=None,
|
||||
@@ -432,6 +494,21 @@ empty_catalog = click.option(
|
||||
is_flag=True,
|
||||
)
|
||||
|
||||
source = click.option(
|
||||
"--source",
|
||||
envvar=None,
|
||||
help="Source to download page from, must be one of hub, git, or local. Defaults to hub.",
|
||||
type=click.Choice(["hub", "git", "local"], case_sensitive=True),
|
||||
default="hub",
|
||||
)
|
||||
|
||||
static = click.option(
|
||||
"--static",
|
||||
help="Generate an additional static_index.html with manifest and catalog built-in.",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
)
|
||||
|
||||
state = click.option(
|
||||
"--state",
|
||||
envvar="DBT_STATE",
|
||||
@@ -500,6 +577,13 @@ target_path = click.option(
|
||||
type=click.Path(),
|
||||
)
|
||||
|
||||
upgrade = click.option(
|
||||
"--upgrade",
|
||||
envvar=None,
|
||||
help="Upgrade packages to the latest version.",
|
||||
is_flag=True,
|
||||
)
|
||||
|
||||
debug_connection = click.option(
|
||||
"--connection",
|
||||
envvar=None,
|
||||
@@ -581,3 +665,10 @@ write_json = click.option(
|
||||
help="Whether or not to write the manifest.json and run_results.json files to the target directory",
|
||||
default=True,
|
||||
)
|
||||
|
||||
show_resource_report = click.option(
|
||||
"--show-resource-report/--no-show-resource-report",
|
||||
default=False,
|
||||
envvar="DBT_SHOW_RESOURCE_REPORT",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import dbt.tracking
|
||||
from dbt.version import installed as installed_version
|
||||
from dbt.adapters.factory import adapter_management, register_adapter
|
||||
from dbt.adapters.factory import adapter_management, register_adapter, get_adapter
|
||||
from dbt.flags import set_flags, get_flag_dict
|
||||
from dbt.cli.exceptions import (
|
||||
ExceptionExit,
|
||||
@@ -9,24 +9,28 @@ from dbt.cli.exceptions import (
|
||||
from dbt.cli.flags import Flags
|
||||
from dbt.config import RuntimeConfig
|
||||
from dbt.config.runtime import load_project, load_profile, UnsetProfile
|
||||
from dbt.events.base_types import EventLevel
|
||||
from dbt.events.functions import fire_event, LOG_VERSION, set_invocation_id, setup_event_logger
|
||||
from dbt.events.types import (
|
||||
CommandCompleted,
|
||||
MainReportVersion,
|
||||
MainReportArgs,
|
||||
MainTrackingUserState,
|
||||
ResourceReport,
|
||||
)
|
||||
from dbt.events.helpers import get_json_string_utcnow
|
||||
from dbt.events.types import MainEncounteredError, MainStackTrace
|
||||
from dbt.exceptions import Exception as DbtException, DbtProjectError, FailFastError
|
||||
from dbt.parser.manifest import ManifestLoader, write_manifest
|
||||
from dbt.parser.manifest import parse_manifest
|
||||
from dbt.profiler import profiler
|
||||
from dbt.tracking import active_user, initialize_from_flags, track_run
|
||||
from dbt.utils import cast_dict_to_dict_of_strings
|
||||
from dbt.plugins import set_up_plugin_manager, get_plugin_manager
|
||||
from dbt.plugins import set_up_plugin_manager
|
||||
|
||||
|
||||
from click import Context
|
||||
from functools import update_wrapper
|
||||
import importlib.util
|
||||
import time
|
||||
import traceback
|
||||
|
||||
@@ -96,6 +100,28 @@ def postflight(func):
|
||||
fire_event(MainStackTrace(stack_trace=traceback.format_exc()))
|
||||
raise ExceptionExit(e)
|
||||
finally:
|
||||
# Fire ResourceReport, but only on systems which support the resource
|
||||
# module. (Skip it on Windows).
|
||||
if importlib.util.find_spec("resource") is not None:
|
||||
import resource
|
||||
|
||||
rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||
fire_event(
|
||||
ResourceReport(
|
||||
command_name=ctx.command.name,
|
||||
command_success=success,
|
||||
command_wall_clock_time=time.perf_counter() - start_func,
|
||||
process_user_time=rusage.ru_utime,
|
||||
process_kernel_time=rusage.ru_stime,
|
||||
process_mem_max_rss=rusage.ru_maxrss,
|
||||
process_in_blocks=rusage.ru_inblock,
|
||||
process_out_blocks=rusage.ru_oublock,
|
||||
),
|
||||
EventLevel.INFO
|
||||
if "flags" in ctx.obj and ctx.obj["flags"].SHOW_RESOURCE_REPORT
|
||||
else None,
|
||||
)
|
||||
|
||||
fire_event(
|
||||
CommandCompleted(
|
||||
command=ctx.command_path,
|
||||
@@ -239,23 +265,16 @@ def manifest(*args0, write=True, write_perf_info=False):
|
||||
raise DbtProjectError("profile, project, and runtime_config required for manifest")
|
||||
|
||||
runtime_config = ctx.obj["runtime_config"]
|
||||
register_adapter(runtime_config)
|
||||
|
||||
# a manifest has already been set on the context, so don't overwrite it
|
||||
if ctx.obj.get("manifest") is None:
|
||||
manifest = ManifestLoader.get_full_manifest(
|
||||
runtime_config,
|
||||
write_perf_info=write_perf_info,
|
||||
ctx.obj["manifest"] = parse_manifest(
|
||||
runtime_config, write_perf_info, write, ctx.obj["flags"].write_json
|
||||
)
|
||||
|
||||
ctx.obj["manifest"] = manifest
|
||||
if write and ctx.obj["flags"].write_json:
|
||||
write_manifest(manifest, runtime_config.project_target_path)
|
||||
pm = get_plugin_manager(runtime_config.project_name)
|
||||
plugin_artifacts = pm.get_manifest_artifacts(manifest)
|
||||
for path, plugin_artifact in plugin_artifacts.items():
|
||||
plugin_artifact.write(path)
|
||||
|
||||
else:
|
||||
register_adapter(runtime_config)
|
||||
adapter = get_adapter(runtime_config)
|
||||
adapter.connections.set_query_header(ctx.obj["manifest"])
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return update_wrapper(wrapper, func)
|
||||
|
||||
@@ -9,10 +9,23 @@ from typing import Iterable, List, Dict, Union, Optional, Any
|
||||
|
||||
from dbt.exceptions import DbtRuntimeError
|
||||
|
||||
|
||||
BOM = BOM_UTF8.decode("utf-8") # '\ufeff'
|
||||
|
||||
|
||||
class Integer(agate.data_types.DataType):
|
||||
def cast(self, d):
|
||||
# by default agate will cast none as a Number
|
||||
# but we need to cast it as an Integer to preserve
|
||||
# the type when merging and unioning tables
|
||||
if type(d) == int or d is None:
|
||||
return d
|
||||
else:
|
||||
raise agate.exceptions.CastError('Can not parse value "%s" as Integer.' % d)
|
||||
|
||||
def jsonify(self, d):
|
||||
return d
|
||||
|
||||
|
||||
class Number(agate.data_types.Number):
|
||||
# undo the change in https://github.com/wireservice/agate/pull/733
|
||||
# i.e. do not cast True and False to numeric 1 and 0
|
||||
@@ -48,6 +61,7 @@ def build_type_tester(
|
||||
) -> agate.TypeTester:
|
||||
|
||||
types = [
|
||||
Integer(null_values=("null", "")),
|
||||
Number(null_values=("null", "")),
|
||||
agate.data_types.Date(null_values=("null", ""), date_format="%Y-%m-%d"),
|
||||
agate.data_types.DateTime(null_values=("null", ""), datetime_format="%Y-%m-%d %H:%M:%S"),
|
||||
@@ -135,12 +149,12 @@ def as_matrix(table):
|
||||
return [r.values() for r in table.rows.values()]
|
||||
|
||||
|
||||
def from_csv(abspath, text_columns):
|
||||
def from_csv(abspath, text_columns, delimiter=","):
|
||||
type_tester = build_type_tester(text_columns=text_columns)
|
||||
with open(abspath, encoding="utf-8") as fp:
|
||||
if fp.read(1) != BOM:
|
||||
fp.seek(0)
|
||||
return agate.Table.from_csv(fp, column_types=type_tester)
|
||||
return agate.Table.from_csv(fp, column_types=type_tester, delimiter=delimiter)
|
||||
|
||||
|
||||
class _NullMarker:
|
||||
@@ -151,7 +165,7 @@ NullableAgateType = Union[agate.data_types.DataType, _NullMarker]
|
||||
|
||||
|
||||
class ColumnTypeBuilder(Dict[str, NullableAgateType]):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
@@ -166,6 +180,13 @@ class ColumnTypeBuilder(Dict[str, NullableAgateType]):
|
||||
elif isinstance(value, _NullMarker):
|
||||
# use the existing value
|
||||
return
|
||||
# when one table column is Number while another is Integer, force the column to Number on merge
|
||||
elif isinstance(value, Integer) and isinstance(existing_type, agate.data_types.Number):
|
||||
# use the existing value
|
||||
return
|
||||
elif isinstance(existing_type, Integer) and isinstance(value, agate.data_types.Number):
|
||||
# overwrite
|
||||
super().__setitem__(key, value)
|
||||
elif not isinstance(value, type(existing_type)):
|
||||
# actual type mismatch!
|
||||
raise DbtRuntimeError(
|
||||
@@ -177,8 +198,9 @@ class ColumnTypeBuilder(Dict[str, NullableAgateType]):
|
||||
result: Dict[str, agate.data_types.DataType] = {}
|
||||
for key, value in self.items():
|
||||
if isinstance(value, _NullMarker):
|
||||
# this is what agate would do.
|
||||
result[key] = agate.data_types.Number()
|
||||
# agate would make it a Number but we'll make it Integer so that if this column
|
||||
# gets merged with another Integer column, it won't get forced to a Number
|
||||
result[key] = Integer()
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
@@ -218,3 +240,12 @@ def merge_tables(tables: List[agate.Table]) -> agate.Table:
|
||||
rows.append(agate.Row(data, column_names))
|
||||
# _is_fork to tell agate that we already made things into `Row`s.
|
||||
return agate.Table(rows, column_names, column_types, _is_fork=True)
|
||||
|
||||
|
||||
def get_column_value_uncased(column_name: str, row: agate.Row) -> Any:
|
||||
"""Get the value of a column in this row, ignoring the casing of the column name."""
|
||||
for key, value in row.items():
|
||||
if key.casefold() == column_name.casefold():
|
||||
return value
|
||||
|
||||
raise KeyError
|
||||
|
||||
@@ -111,7 +111,7 @@ def checkout(cwd, repo, revision=None):
|
||||
def get_current_sha(cwd):
|
||||
out, err = run_cmd(cwd, ["git", "rev-parse", "HEAD"], env={"LC_ALL": "C"})
|
||||
|
||||
return out.decode("utf-8")
|
||||
return out.decode("utf-8").strip()
|
||||
|
||||
|
||||
def remove_remote(cwd):
|
||||
|
||||
@@ -191,7 +191,7 @@ NativeSandboxEnvironment.template_class = NativeSandboxTemplate # type: ignore
|
||||
|
||||
|
||||
class TemplateCache:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.file_cache: Dict[str, jinja2.Template] = {}
|
||||
|
||||
def get_node_template(self, node) -> jinja2.Template:
|
||||
|
||||
@@ -4,7 +4,6 @@ import json
|
||||
import networkx as nx # type: ignore
|
||||
import os
|
||||
import pickle
|
||||
import sqlparse
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
@@ -36,6 +35,7 @@ from dbt.node_types import NodeType, ModelLanguage
|
||||
from dbt.events.format import pluralize
|
||||
import dbt.tracking
|
||||
import dbt.task.list as list_task
|
||||
import sqlparse
|
||||
|
||||
graph_file_name = "graph.gpickle"
|
||||
|
||||
@@ -125,7 +125,7 @@ def _get_tests_for_node(manifest: Manifest, unique_id: UniqueID) -> List[UniqueI
|
||||
|
||||
|
||||
class Linker:
|
||||
def __init__(self, data=None):
|
||||
def __init__(self, data=None) -> None:
|
||||
if data is None:
|
||||
data = {}
|
||||
self.graph = nx.DiGraph(**data)
|
||||
@@ -183,14 +183,16 @@ class Linker:
|
||||
def link_graph(self, manifest: Manifest):
|
||||
for source in manifest.sources.values():
|
||||
self.add_node(source.unique_id)
|
||||
for semantic_model in manifest.semantic_models.values():
|
||||
self.add_node(semantic_model.unique_id)
|
||||
for node in manifest.nodes.values():
|
||||
self.link_node(node, manifest)
|
||||
for semantic_model in manifest.semantic_models.values():
|
||||
self.link_node(semantic_model, manifest)
|
||||
for exposure in manifest.exposures.values():
|
||||
self.link_node(exposure, manifest)
|
||||
for metric in manifest.metrics.values():
|
||||
self.link_node(metric, manifest)
|
||||
for saved_query in manifest.saved_queries.values():
|
||||
self.link_node(saved_query, manifest)
|
||||
|
||||
cycle = self.find_cycles()
|
||||
|
||||
@@ -274,7 +276,7 @@ class Linker:
|
||||
|
||||
|
||||
class Compiler:
|
||||
def __init__(self, config):
|
||||
def __init__(self, config) -> None:
|
||||
self.config = config
|
||||
|
||||
def initialize(self):
|
||||
@@ -320,6 +322,10 @@ class Compiler:
|
||||
if model.compiled_code is None:
|
||||
raise DbtRuntimeError("Cannot inject ctes into an uncompiled node", model)
|
||||
|
||||
# tech debt: safe flag/arg access (#6259)
|
||||
if not getattr(self.config.args, "inject_ephemeral_ctes", True):
|
||||
return (model, [])
|
||||
|
||||
# extra_ctes_injected flag says that we've already recursively injected the ctes
|
||||
if model.extra_ctes_injected:
|
||||
return (model, model.extra_ctes)
|
||||
@@ -378,16 +384,16 @@ class Compiler:
|
||||
|
||||
_add_prepended_cte(prepended_ctes, InjectedCTE(id=cte.id, sql=sql))
|
||||
|
||||
injected_sql = inject_ctes_into_sql(
|
||||
model.compiled_code,
|
||||
prepended_ctes,
|
||||
)
|
||||
# Check again before updating for multi-threading
|
||||
if not model.extra_ctes_injected:
|
||||
injected_sql = inject_ctes_into_sql(
|
||||
model.compiled_code,
|
||||
prepended_ctes,
|
||||
)
|
||||
model.extra_ctes_injected = True
|
||||
model._pre_injected_sql = model.compiled_code
|
||||
model.compiled_code = injected_sql
|
||||
model.extra_ctes = prepended_ctes
|
||||
model.extra_ctes_injected = True
|
||||
|
||||
# if model.extra_ctes is not set to prepended ctes, something went wrong
|
||||
return model, model.extra_ctes
|
||||
@@ -523,6 +529,12 @@ class Compiler:
|
||||
the node's raw_code into compiled_code, and then calls the
|
||||
recursive method to "prepend" the ctes.
|
||||
"""
|
||||
# Make sure Lexer for sqlparse 0.4.4 is initialized
|
||||
from sqlparse.lexer import Lexer # type: ignore
|
||||
|
||||
if hasattr(Lexer, "get_default_instance"):
|
||||
Lexer.get_default_instance()
|
||||
|
||||
node = self._compile_code(node, manifest, extra_context)
|
||||
|
||||
node, _ = self._recursively_prepend_ctes(node, manifest, extra_context)
|
||||
|
||||
@@ -83,7 +83,7 @@ class Profile(HasCredentials):
|
||||
user_config: UserConfig,
|
||||
threads: int,
|
||||
credentials: Credentials,
|
||||
):
|
||||
) -> None:
|
||||
"""Explicitly defining `__init__` to work around bug in Python 3.9.7
|
||||
https://bugs.python.org/issue45081
|
||||
"""
|
||||
|
||||
@@ -16,8 +16,12 @@ import os
|
||||
|
||||
from dbt.flags import get_flags
|
||||
from dbt import deprecations
|
||||
from dbt.constants import DEPENDENCIES_FILE_NAME, PACKAGES_FILE_NAME
|
||||
from dbt.clients.system import path_exists, resolve_path_from_base, load_file_contents
|
||||
from dbt.constants import (
|
||||
DEPENDENCIES_FILE_NAME,
|
||||
PACKAGES_FILE_NAME,
|
||||
PACKAGE_LOCK_HASH_KEY,
|
||||
)
|
||||
from dbt.clients.system import path_exists, load_file_contents
|
||||
from dbt.clients.yaml_helper import load_yaml_text
|
||||
from dbt.contracts.connection import QueryComment
|
||||
from dbt.exceptions import (
|
||||
@@ -94,16 +98,17 @@ def _load_yaml(path):
|
||||
return load_yaml_text(contents)
|
||||
|
||||
|
||||
def package_and_project_data_from_root(project_root):
|
||||
package_filepath = resolve_path_from_base(PACKAGES_FILE_NAME, project_root)
|
||||
dependencies_filepath = resolve_path_from_base(DEPENDENCIES_FILE_NAME, project_root)
|
||||
def load_yml_dict(file_path):
|
||||
ret = {}
|
||||
if path_exists(file_path):
|
||||
ret = _load_yaml(file_path) or {}
|
||||
return ret
|
||||
|
||||
packages_yml_dict = {}
|
||||
dependencies_yml_dict = {}
|
||||
if path_exists(package_filepath):
|
||||
packages_yml_dict = _load_yaml(package_filepath) or {}
|
||||
if path_exists(dependencies_filepath):
|
||||
dependencies_yml_dict = _load_yaml(dependencies_filepath) or {}
|
||||
|
||||
def package_and_project_data_from_root(project_root):
|
||||
|
||||
packages_yml_dict = load_yml_dict(f"{project_root}/{PACKAGES_FILE_NAME}")
|
||||
dependencies_yml_dict = load_yml_dict(f"{project_root}/{DEPENDENCIES_FILE_NAME}")
|
||||
|
||||
if "packages" in packages_yml_dict and "packages" in dependencies_yml_dict:
|
||||
msg = "The 'packages' key cannot be specified in both packages.yml and dependencies.yml"
|
||||
@@ -123,10 +128,21 @@ def package_and_project_data_from_root(project_root):
|
||||
return packages_dict, packages_specified_path
|
||||
|
||||
|
||||
def package_config_from_data(packages_data: Dict[str, Any]) -> PackageConfig:
|
||||
def package_config_from_data(
|
||||
packages_data: Dict[str, Any],
|
||||
unrendered_packages_data: Optional[Dict[str, Any]] = None,
|
||||
) -> PackageConfig:
|
||||
if not packages_data:
|
||||
packages_data = {"packages": []}
|
||||
|
||||
# this depends on the two lists being in the same order
|
||||
if unrendered_packages_data:
|
||||
unrendered_packages_data = deepcopy(unrendered_packages_data)
|
||||
for i in range(0, len(packages_data.get("packages", []))):
|
||||
packages_data["packages"][i]["unrendered"] = unrendered_packages_data["packages"][i]
|
||||
|
||||
if PACKAGE_LOCK_HASH_KEY in packages_data:
|
||||
packages_data.pop(PACKAGE_LOCK_HASH_KEY)
|
||||
try:
|
||||
PackageConfig.validate(packages_data)
|
||||
packages = PackageConfig.from_dict(packages_data)
|
||||
@@ -319,7 +335,7 @@ class PartialProject(RenderComponents):
|
||||
|
||||
def render_package_metadata(self, renderer: PackageRenderer) -> ProjectPackageMetadata:
|
||||
packages_data = renderer.render_data(self.packages_dict)
|
||||
packages_config = package_config_from_data(packages_data)
|
||||
packages_config = package_config_from_data(packages_data, self.packages_dict)
|
||||
if not self.project_name:
|
||||
raise DbtProjectError("Package dbt_project.yml must have a name!")
|
||||
return ProjectPackageMetadata(self.project_name, packages_config.packages)
|
||||
@@ -426,8 +442,11 @@ class PartialProject(RenderComponents):
|
||||
sources: Dict[str, Any]
|
||||
tests: Dict[str, Any]
|
||||
metrics: Dict[str, Any]
|
||||
semantic_models: Dict[str, Any]
|
||||
saved_queries: Dict[str, Any]
|
||||
exposures: Dict[str, Any]
|
||||
vars_value: VarProvider
|
||||
dbt_cloud: Dict[str, Any]
|
||||
|
||||
dispatch = cfg.dispatch
|
||||
models = cfg.models
|
||||
@@ -436,6 +455,8 @@ class PartialProject(RenderComponents):
|
||||
sources = cfg.sources
|
||||
tests = cfg.tests
|
||||
metrics = cfg.metrics
|
||||
semantic_models = cfg.semantic_models
|
||||
saved_queries = cfg.saved_queries
|
||||
exposures = cfg.exposures
|
||||
if cfg.vars is None:
|
||||
vars_dict: Dict[str, Any] = {}
|
||||
@@ -449,8 +470,9 @@ class PartialProject(RenderComponents):
|
||||
on_run_end: List[str] = value_or(cfg.on_run_end, [])
|
||||
|
||||
query_comment = _query_comment_from_cfg(cfg.query_comment)
|
||||
|
||||
packages: PackageConfig = package_config_from_data(rendered.packages_dict)
|
||||
packages: PackageConfig = package_config_from_data(
|
||||
rendered.packages_dict, unrendered.packages_dict
|
||||
)
|
||||
selectors = selector_config_from_data(rendered.selectors_dict)
|
||||
manifest_selectors: Dict[str, Any] = {}
|
||||
if rendered.selectors_dict and rendered.selectors_dict["selectors"]:
|
||||
@@ -459,6 +481,8 @@ class PartialProject(RenderComponents):
|
||||
manifest_selectors = SelectorDict.parse_from_selectors_list(
|
||||
rendered.selectors_dict["selectors"]
|
||||
)
|
||||
dbt_cloud = cfg.dbt_cloud
|
||||
|
||||
project = Project(
|
||||
project_name=name,
|
||||
version=version,
|
||||
@@ -492,12 +516,15 @@ class PartialProject(RenderComponents):
|
||||
sources=sources,
|
||||
tests=tests,
|
||||
metrics=metrics,
|
||||
semantic_models=semantic_models,
|
||||
saved_queries=saved_queries,
|
||||
exposures=exposures,
|
||||
vars=vars_value,
|
||||
config_version=cfg.config_version,
|
||||
unrendered=unrendered,
|
||||
project_env_vars=project_env_vars,
|
||||
restrict_access=cfg.restrict_access,
|
||||
dbt_cloud=dbt_cloud,
|
||||
)
|
||||
# sanity check - this means an internal issue
|
||||
project.validate()
|
||||
@@ -541,6 +568,7 @@ class PartialProject(RenderComponents):
|
||||
packages_specified_path,
|
||||
) = package_and_project_data_from_root(project_root)
|
||||
selectors_dict = selector_data_from_root(project_root)
|
||||
|
||||
return cls.from_dicts(
|
||||
project_root=project_root,
|
||||
project_dict=project_dict,
|
||||
@@ -598,6 +626,8 @@ class Project:
|
||||
sources: Dict[str, Any]
|
||||
tests: Dict[str, Any]
|
||||
metrics: Dict[str, Any]
|
||||
semantic_models: Dict[str, Any]
|
||||
saved_queries: Dict[str, Any]
|
||||
exposures: Dict[str, Any]
|
||||
vars: VarProvider
|
||||
dbt_version: List[VersionSpecifier]
|
||||
@@ -609,6 +639,7 @@ class Project:
|
||||
unrendered: RenderComponents
|
||||
project_env_vars: Dict[str, Any]
|
||||
restrict_access: bool
|
||||
dbt_cloud: Dict[str, Any]
|
||||
|
||||
@property
|
||||
def all_source_paths(self) -> List[str]:
|
||||
@@ -673,11 +704,14 @@ class Project:
|
||||
"sources": self.sources,
|
||||
"tests": self.tests,
|
||||
"metrics": self.metrics,
|
||||
"semantic-models": self.semantic_models,
|
||||
"saved-queries": self.saved_queries,
|
||||
"exposures": self.exposures,
|
||||
"vars": self.vars.to_dict(),
|
||||
"require-dbt-version": [v.to_version_string() for v in self.dbt_version],
|
||||
"config-version": self.config_version,
|
||||
"restrict-access": self.restrict_access,
|
||||
"dbt-cloud": self.dbt_cloud,
|
||||
}
|
||||
)
|
||||
if self.query_comment:
|
||||
|
||||
@@ -74,7 +74,7 @@ def _list_if_none_or_string(value):
|
||||
|
||||
|
||||
class ProjectPostprocessor(Dict[Keypath, Callable[[Any], Any]]):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self[("on-run-start",)] = _list_if_none_or_string
|
||||
|
||||
@@ -167,6 +167,8 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
|
||||
sources=project.sources,
|
||||
tests=project.tests,
|
||||
metrics=project.metrics,
|
||||
semantic_models=project.semantic_models,
|
||||
saved_queries=project.saved_queries,
|
||||
exposures=project.exposures,
|
||||
vars=project.vars,
|
||||
config_version=project.config_version,
|
||||
@@ -182,6 +184,7 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
|
||||
args=args,
|
||||
cli_vars=cli_vars,
|
||||
dependencies=dependencies,
|
||||
dbt_cloud=project.dbt_cloud,
|
||||
)
|
||||
|
||||
# Called by 'load_projects' in this class
|
||||
@@ -322,6 +325,8 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
|
||||
"sources": self._get_config_paths(self.sources),
|
||||
"tests": self._get_config_paths(self.tests),
|
||||
"metrics": self._get_config_paths(self.metrics),
|
||||
"semantic_models": self._get_config_paths(self.semantic_models),
|
||||
"saved_queries": self._get_config_paths(self.saved_queries),
|
||||
"exposures": self._get_config_paths(self.exposures),
|
||||
}
|
||||
|
||||
@@ -404,7 +409,7 @@ class RuntimeConfig(Project, Profile, AdapterRequiredConfig):
|
||||
|
||||
|
||||
class UnsetCredentials(Credentials):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("", "")
|
||||
|
||||
@property
|
||||
|
||||
@@ -9,8 +9,11 @@ PIN_PACKAGE_URL = (
|
||||
"https://docs.getdbt.com/docs/package-management#section-specifying-package-versions"
|
||||
)
|
||||
|
||||
DBT_PROJECT_FILE_NAME = "dbt_project.yml"
|
||||
PACKAGES_FILE_NAME = "packages.yml"
|
||||
DEPENDENCIES_FILE_NAME = "dependencies.yml"
|
||||
PACKAGE_LOCK_FILE_NAME = "package-lock.yml"
|
||||
MANIFEST_FILE_NAME = "manifest.json"
|
||||
SEMANTIC_MANIFEST_FILE_NAME = "semantic_manifest.json"
|
||||
PARTIAL_PARSE_FILE_NAME = "partial_parse.msgpack"
|
||||
PACKAGE_LOCK_HASH_KEY = "sha1_hash"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Dict, NoReturn, Optional, Mapping, Iterable, Set, List
|
||||
from typing import Any, Callable, Dict, NoReturn, Optional, Mapping, Iterable, Set, List
|
||||
import threading
|
||||
|
||||
from dbt.flags import get_flags
|
||||
@@ -86,33 +88,29 @@ def get_context_modules() -> Dict[str, Dict[str, Any]]:
|
||||
|
||||
|
||||
class ContextMember:
|
||||
def __init__(self, value, name=None):
|
||||
def __init__(self, value: Any, name: Optional[str] = None) -> None:
|
||||
self.name = name
|
||||
self.inner = value
|
||||
|
||||
def key(self, default):
|
||||
def key(self, default: str) -> str:
|
||||
if self.name is None:
|
||||
return default
|
||||
return self.name
|
||||
|
||||
|
||||
def contextmember(value):
|
||||
if isinstance(value, str):
|
||||
return lambda v: ContextMember(v, name=value)
|
||||
return ContextMember(value)
|
||||
def contextmember(value: Optional[str] = None) -> Callable:
|
||||
return lambda v: ContextMember(v, name=value)
|
||||
|
||||
|
||||
def contextproperty(value):
|
||||
if isinstance(value, str):
|
||||
return lambda v: ContextMember(property(v), name=value)
|
||||
return ContextMember(property(value))
|
||||
def contextproperty(value: Optional[str] = None) -> Callable:
|
||||
return lambda v: ContextMember(property(v), name=value)
|
||||
|
||||
|
||||
class ContextMeta(type):
|
||||
def __new__(mcls, name, bases, dct):
|
||||
context_members = {}
|
||||
context_attrs = {}
|
||||
new_dct = {}
|
||||
def __new__(mcls, name, bases, dct: Dict[str, Any]) -> ContextMeta:
|
||||
context_members: Dict[str, Any] = {}
|
||||
context_attrs: Dict[str, Any] = {}
|
||||
new_dct: Dict[str, Any] = {}
|
||||
|
||||
for base in bases:
|
||||
context_members.update(getattr(base, "_context_members_", {}))
|
||||
@@ -148,27 +146,28 @@ class Var:
|
||||
return self._cli_vars
|
||||
|
||||
@property
|
||||
def node_name(self):
|
||||
def node_name(self) -> str:
|
||||
if self._node is not None:
|
||||
return self._node.name
|
||||
else:
|
||||
return "<Configuration>"
|
||||
|
||||
def get_missing_var(self, var_name):
|
||||
raise RequiredVarNotFoundError(var_name, self._merged, self._node)
|
||||
def get_missing_var(self, var_name: str) -> NoReturn:
|
||||
# TODO function name implies a non exception resolution
|
||||
raise RequiredVarNotFoundError(var_name, dict(self._merged), self._node)
|
||||
|
||||
def has_var(self, var_name: str):
|
||||
def has_var(self, var_name: str) -> bool:
|
||||
return var_name in self._merged
|
||||
|
||||
def get_rendered_var(self, var_name):
|
||||
def get_rendered_var(self, var_name: str) -> Any:
|
||||
raw = self._merged[var_name]
|
||||
# if bool/int/float/etc are passed in, don't compile anything
|
||||
if not isinstance(raw, str):
|
||||
return raw
|
||||
|
||||
return get_rendered(raw, self._context)
|
||||
return get_rendered(raw, dict(self._context))
|
||||
|
||||
def __call__(self, var_name, default=_VAR_NOTSET):
|
||||
def __call__(self, var_name: str, default: Any = _VAR_NOTSET) -> Any:
|
||||
if self.has_var(var_name):
|
||||
return self.get_rendered_var(var_name)
|
||||
elif default is not self._VAR_NOTSET:
|
||||
@@ -178,13 +177,17 @@ class Var:
|
||||
|
||||
|
||||
class BaseContext(metaclass=ContextMeta):
|
||||
# subclass is TargetContext
|
||||
def __init__(self, cli_vars):
|
||||
self._ctx = {}
|
||||
self.cli_vars = cli_vars
|
||||
self.env_vars = {}
|
||||
# Set by ContextMeta
|
||||
_context_members_: Dict[str, Any]
|
||||
_context_attrs_: Dict[str, Any]
|
||||
|
||||
def generate_builtins(self):
|
||||
# subclass is TargetContext
|
||||
def __init__(self, cli_vars: Dict[str, Any]) -> None:
|
||||
self._ctx: Dict[str, Any] = {}
|
||||
self.cli_vars: Dict[str, Any] = cli_vars
|
||||
self.env_vars: Dict[str, Any] = {}
|
||||
|
||||
def generate_builtins(self) -> Dict[str, Any]:
|
||||
builtins: Dict[str, Any] = {}
|
||||
for key, value in self._context_members_.items():
|
||||
if hasattr(value, "__get__"):
|
||||
@@ -194,14 +197,14 @@ class BaseContext(metaclass=ContextMeta):
|
||||
return builtins
|
||||
|
||||
# no dbtClassMixin so this is not an actual override
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
self._ctx["context"] = self._ctx
|
||||
builtins = self.generate_builtins()
|
||||
self._ctx["builtins"] = builtins
|
||||
self._ctx.update(builtins)
|
||||
return self._ctx
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def dbt_version(self) -> str:
|
||||
"""The `dbt_version` variable returns the installed version of dbt that
|
||||
is currently running. It can be used for debugging or auditing
|
||||
@@ -221,7 +224,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
"""
|
||||
return dbt_version
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def var(self) -> Var:
|
||||
"""Variables can be passed from your `dbt_project.yml` file into models
|
||||
during compilation. These variables are useful for configuring packages
|
||||
@@ -290,7 +293,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
"""
|
||||
return Var(self._ctx, self.cli_vars)
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def env_var(self, var: str, default: Optional[str] = None) -> str:
|
||||
"""The env_var() function. Return the environment variable named 'var'.
|
||||
If there is no such environment variable set, return the default.
|
||||
@@ -318,7 +321,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
|
||||
if os.environ.get("DBT_MACRO_DEBUGGING"):
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def debug():
|
||||
"""Enter a debugger at this line in the compiled jinja code."""
|
||||
@@ -357,7 +360,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
"""
|
||||
raise MacroReturn(data)
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def fromjson(string: str, default: Any = None) -> Any:
|
||||
"""The `fromjson` context method can be used to deserialize a json
|
||||
@@ -378,7 +381,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def tojson(value: Any, default: Any = None, sort_keys: bool = False) -> Any:
|
||||
"""The `tojson` context method can be used to serialize a Python
|
||||
@@ -401,7 +404,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def fromyaml(value: str, default: Any = None) -> Any:
|
||||
"""The fromyaml context method can be used to deserialize a yaml string
|
||||
@@ -432,7 +435,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
|
||||
# safe_dump defaults to sort_keys=True, but we act like json.dumps (the
|
||||
# opposite)
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def toyaml(
|
||||
value: Any, default: Optional[str] = None, sort_keys: bool = False
|
||||
@@ -477,7 +480,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
except TypeError:
|
||||
return default
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def set_strict(value: Iterable[Any]) -> Set[Any]:
|
||||
"""The `set_strict` context method can be used to convert any iterable
|
||||
@@ -519,7 +522,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
except TypeError:
|
||||
return default
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def zip_strict(*args: Iterable[Any]) -> Iterable[Any]:
|
||||
"""The `zip_strict` context method can be used to used to return
|
||||
@@ -541,7 +544,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
except TypeError as e:
|
||||
raise ZipStrictWrongTypeError(e)
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def log(msg: str, info: bool = False) -> str:
|
||||
"""Logs a line to either the log file or stdout.
|
||||
@@ -562,7 +565,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
fire_event(JinjaLogDebug(msg=msg, node_info=get_node_info()))
|
||||
return ""
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def run_started_at(self) -> Optional[datetime.datetime]:
|
||||
"""`run_started_at` outputs the timestamp that this run started, e.g.
|
||||
`2017-04-21 01:23:45.678`. The `run_started_at` variable is a Python
|
||||
@@ -590,19 +593,19 @@ class BaseContext(metaclass=ContextMeta):
|
||||
else:
|
||||
return None
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def invocation_id(self) -> Optional[str]:
|
||||
"""invocation_id outputs a UUID generated for this dbt run (useful for
|
||||
auditing)
|
||||
"""
|
||||
return get_invocation_id()
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def thread_id(self) -> str:
|
||||
"""thread_id outputs an ID for the current thread (useful for auditing)"""
|
||||
return threading.current_thread().name
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def modules(self) -> Dict[str, Any]:
|
||||
"""The `modules` variable in the Jinja context contains useful Python
|
||||
modules for operating on data.
|
||||
@@ -627,7 +630,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
""" # noqa
|
||||
return get_context_modules()
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def flags(self) -> Any:
|
||||
"""The `flags` variable contains true/false values for flags provided
|
||||
on the command line.
|
||||
@@ -644,7 +647,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
"""
|
||||
return flags_module.get_flag_obj()
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def print(msg: str) -> str:
|
||||
"""Prints a line to stdout.
|
||||
@@ -662,7 +665,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
print(msg)
|
||||
return ""
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def diff_of_two_dicts(
|
||||
dict_a: Dict[str, List[str]], dict_b: Dict[str, List[str]]
|
||||
@@ -691,7 +694,7 @@ class BaseContext(metaclass=ContextMeta):
|
||||
dict_diff.update({k: dict_a[k]})
|
||||
return dict_diff
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
@staticmethod
|
||||
def local_md5(value: str) -> str:
|
||||
"""Calculates an MD5 hash of the given string.
|
||||
|
||||
@@ -19,7 +19,7 @@ class ConfiguredContext(TargetContext):
|
||||
super().__init__(config.to_target_dict(), config.cli_vars)
|
||||
self.config = config
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def project_name(self) -> str:
|
||||
return self.config.project_name
|
||||
|
||||
@@ -80,11 +80,11 @@ class SchemaYamlContext(ConfiguredContext):
|
||||
self._project_name = project_name
|
||||
self.schema_yaml_vars = schema_yaml_vars
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def var(self) -> ConfiguredVar:
|
||||
return ConfiguredVar(self._ctx, self.config, self._project_name)
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def env_var(self, var: str, default: Optional[str] = None) -> str:
|
||||
return_value = None
|
||||
if var.startswith(SECRET_ENV_PREFIX):
|
||||
@@ -113,7 +113,7 @@ class MacroResolvingContext(ConfiguredContext):
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def var(self) -> ConfiguredVar:
|
||||
return ConfiguredVar(self._ctx, self.config, self.config.project_name)
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ class UnrenderedConfig(ConfigSource):
|
||||
model_configs = unrendered.get("tests")
|
||||
elif resource_type == NodeType.Metric:
|
||||
model_configs = unrendered.get("metrics")
|
||||
elif resource_type == NodeType.SemanticModel:
|
||||
model_configs = unrendered.get("semantic_models")
|
||||
elif resource_type == NodeType.SavedQuery:
|
||||
model_configs = unrendered.get("saved_queries")
|
||||
elif resource_type == NodeType.Exposure:
|
||||
model_configs = unrendered.get("exposures")
|
||||
else:
|
||||
@@ -70,6 +74,10 @@ class RenderedConfig(ConfigSource):
|
||||
model_configs = self.project.tests
|
||||
elif resource_type == NodeType.Metric:
|
||||
model_configs = self.project.metrics
|
||||
elif resource_type == NodeType.SemanticModel:
|
||||
model_configs = self.project.semantic_models
|
||||
elif resource_type == NodeType.SavedQuery:
|
||||
model_configs = self.project.saved_queries
|
||||
elif resource_type == NodeType.Exposure:
|
||||
model_configs = self.project.exposures
|
||||
else:
|
||||
@@ -189,9 +197,21 @@ class ContextConfigGenerator(BaseContextConfigGenerator[C]):
|
||||
|
||||
def _update_from_config(self, result: C, partial: Dict[str, Any], validate: bool = False) -> C:
|
||||
translated = self._active_project.credentials.translate_aliases(partial)
|
||||
return result.update_from(
|
||||
translated = self.translate_hook_names(translated)
|
||||
updated = result.update_from(
|
||||
translated, self._active_project.credentials.type, validate=validate
|
||||
)
|
||||
return updated
|
||||
|
||||
def translate_hook_names(self, project_dict):
|
||||
# This is a kind of kludge because the fix for #6411 specifically allowed misspelling
|
||||
# the hook field names in dbt_project.yml, which only ever worked because we didn't
|
||||
# run validate on the dbt_project configs.
|
||||
if "pre_hook" in project_dict:
|
||||
project_dict["pre-hook"] = project_dict.pop("pre_hook")
|
||||
if "post_hook" in project_dict:
|
||||
project_dict["post-hook"] = project_dict.pop("post_hook")
|
||||
return project_dict
|
||||
|
||||
def calculate_node_config_dict(
|
||||
self,
|
||||
|
||||
@@ -24,7 +24,7 @@ class DocsRuntimeContext(SchemaYamlContext):
|
||||
self.node = node
|
||||
self.manifest = manifest
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def doc(self, *args: str) -> str:
|
||||
"""The `doc` function is used to reference docs blocks in schema.yml
|
||||
files. It is analogous to the `ref` function. For more information,
|
||||
|
||||
@@ -2,7 +2,6 @@ import functools
|
||||
from typing import NoReturn
|
||||
|
||||
from dbt.events.functions import warn_or_error
|
||||
from dbt.events.helpers import env_secrets, scrub_secrets
|
||||
from dbt.events.types import JinjaLogWarning
|
||||
|
||||
from dbt.exceptions import (
|
||||
@@ -26,6 +25,8 @@ from dbt.exceptions import (
|
||||
ContractError,
|
||||
ColumnTypeMissingError,
|
||||
FailFastError,
|
||||
scrub_secrets,
|
||||
env_secrets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class MacroResolver:
|
||||
self._build_internal_packages_namespace()
|
||||
self._build_macros_by_name()
|
||||
|
||||
def _build_internal_packages_namespace(self):
|
||||
def _build_internal_packages_namespace(self) -> None:
|
||||
# Iterate in reverse-order and overwrite: the packages that are first
|
||||
# in the list are the ones we want to "win".
|
||||
self.internal_packages_namespace: MacroNamespace = {}
|
||||
@@ -56,7 +56,7 @@ class MacroResolver:
|
||||
# root package namespace
|
||||
# non-internal packages (that aren't local or root)
|
||||
# dbt internal packages
|
||||
def _build_macros_by_name(self):
|
||||
def _build_macros_by_name(self) -> None:
|
||||
macros_by_name = {}
|
||||
|
||||
# all internal packages (already in the right order)
|
||||
@@ -78,7 +78,7 @@ class MacroResolver:
|
||||
self,
|
||||
package_namespaces: Dict[str, MacroNamespace],
|
||||
macro: Macro,
|
||||
):
|
||||
) -> None:
|
||||
if macro.package_name in package_namespaces:
|
||||
namespace = package_namespaces[macro.package_name]
|
||||
else:
|
||||
@@ -89,7 +89,7 @@ class MacroResolver:
|
||||
raise DuplicateMacroNameError(macro, macro, macro.package_name)
|
||||
package_namespaces[macro.package_name][macro.name] = macro
|
||||
|
||||
def add_macro(self, macro: Macro):
|
||||
def add_macro(self, macro: Macro) -> None:
|
||||
macro_name: str = macro.name
|
||||
|
||||
# internal macros (from plugins) will be processed separately from
|
||||
@@ -103,11 +103,11 @@ class MacroResolver:
|
||||
if macro.package_name == self.root_project_name:
|
||||
self.root_package_macros[macro_name] = macro
|
||||
|
||||
def add_macros(self):
|
||||
def add_macros(self) -> None:
|
||||
for macro in self.macros.values():
|
||||
self.add_macro(macro)
|
||||
|
||||
def get_macro(self, local_package, macro_name):
|
||||
def get_macro(self, local_package, macro_name) -> Optional[Macro]:
|
||||
local_package_macros = {}
|
||||
# If the macro is explicitly prefixed with an internal namespace
|
||||
# (e.g. 'dbt.some_macro'), look there first
|
||||
@@ -125,7 +125,7 @@ class MacroResolver:
|
||||
return self.macros_by_name[macro_name]
|
||||
return None
|
||||
|
||||
def get_macro_id(self, local_package, macro_name):
|
||||
def get_macro_id(self, local_package, macro_name) -> Optional[str]:
|
||||
macro = self.get_macro(local_package, macro_name)
|
||||
if macro is None:
|
||||
return None
|
||||
|
||||
@@ -67,7 +67,7 @@ class ManifestContext(ConfiguredContext):
|
||||
dct.update(self.namespace)
|
||||
return dct
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def context_macro_stack(self):
|
||||
return self.macro_stack
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ from dbt.exceptions import (
|
||||
MetricArgsError,
|
||||
MissingConfigError,
|
||||
OperationsCannotRefEphemeralNodesError,
|
||||
PackageNotInDepsError,
|
||||
ParsingError,
|
||||
RefBadContextError,
|
||||
RefArgsError,
|
||||
@@ -618,7 +617,7 @@ class RuntimeMetricResolver(BaseMetricResolver):
|
||||
target_package=target_package,
|
||||
)
|
||||
|
||||
return ResolvedMetricReference(target_metric, self.manifest, self.Relation)
|
||||
return ResolvedMetricReference(target_metric, self.manifest)
|
||||
|
||||
|
||||
# `var` implementations.
|
||||
@@ -638,10 +637,8 @@ class ModelConfiguredVar(Var):
|
||||
package_name = self._node.package_name
|
||||
|
||||
if package_name != self._config.project_name:
|
||||
if package_name not in dependencies:
|
||||
# I don't think this is actually reachable
|
||||
raise PackageNotInDepsError(package_name, node=self._node)
|
||||
yield dependencies[package_name]
|
||||
if package_name in dependencies:
|
||||
yield dependencies[package_name]
|
||||
yield self._config
|
||||
|
||||
def _generate_merged(self) -> Mapping[str, Any]:
|
||||
@@ -754,19 +751,19 @@ class ProviderContext(ManifestContext):
|
||||
self.model,
|
||||
)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def dbt_metadata_envs(self) -> Dict[str, str]:
|
||||
return get_metadata_vars()
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def invocation_args_dict(self):
|
||||
return args_to_dict(self.config.args)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def _sql_results(self) -> Dict[str, Optional[AttrDict]]:
|
||||
return self.sql_results
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def load_result(self, name: str) -> Optional[AttrDict]:
|
||||
if name in self.sql_results:
|
||||
# handle the special case of "main" macro
|
||||
@@ -787,7 +784,7 @@ class ProviderContext(ManifestContext):
|
||||
# Handle trying to load a result that was never stored
|
||||
return None
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def store_result(
|
||||
self, name: str, response: Any, agate_table: Optional[agate.Table] = None
|
||||
) -> str:
|
||||
@@ -803,7 +800,7 @@ class ProviderContext(ManifestContext):
|
||||
)
|
||||
return ""
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def store_raw_result(
|
||||
self,
|
||||
name: str,
|
||||
@@ -815,7 +812,7 @@ class ProviderContext(ManifestContext):
|
||||
response = AdapterResponse(_message=message, code=code, rows_affected=rows_affected)
|
||||
return self.store_result(name, response, agate_table)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def validation(self):
|
||||
def validate_any(*args) -> Callable[[T], None]:
|
||||
def inner(value: T) -> None:
|
||||
@@ -836,7 +833,7 @@ class ProviderContext(ManifestContext):
|
||||
}
|
||||
)
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def write(self, payload: str) -> str:
|
||||
# macros/source defs aren't 'writeable'.
|
||||
if isinstance(self.model, (Macro, SourceDefinition)):
|
||||
@@ -845,11 +842,11 @@ class ProviderContext(ManifestContext):
|
||||
self.model.write_node(self.config.project_root, self.model.build_path, payload)
|
||||
return ""
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def render(self, string: str) -> str:
|
||||
return get_rendered(string, self._ctx, self.model)
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def try_or_compiler_error(
|
||||
self, message_if_exception: str, func: Callable, *args, **kwargs
|
||||
) -> Any:
|
||||
@@ -858,21 +855,32 @@ class ProviderContext(ManifestContext):
|
||||
except Exception:
|
||||
raise CompilationError(message_if_exception, self.model)
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def load_agate_table(self) -> agate.Table:
|
||||
if not isinstance(self.model, SeedNode):
|
||||
raise LoadAgateTableNotSeedError(self.model.resource_type, node=self.model)
|
||||
assert self.model.root_path
|
||||
path = os.path.join(self.model.root_path, self.model.original_file_path)
|
||||
|
||||
# include package_path for seeds defined in packages
|
||||
package_path = (
|
||||
os.path.join(self.config.packages_install_path, self.model.package_name)
|
||||
if self.model.package_name != self.config.project_name
|
||||
else "."
|
||||
)
|
||||
path = os.path.join(self.config.project_root, package_path, self.model.original_file_path)
|
||||
if not os.path.exists(path):
|
||||
assert self.model.root_path
|
||||
path = os.path.join(self.model.root_path, self.model.original_file_path)
|
||||
|
||||
column_types = self.model.config.column_types
|
||||
delimiter = self.model.config.delimiter
|
||||
try:
|
||||
table = agate_helper.from_csv(path, text_columns=column_types)
|
||||
table = agate_helper.from_csv(path, text_columns=column_types, delimiter=delimiter)
|
||||
except ValueError as e:
|
||||
raise LoadAgateTableValueError(e, node=self.model)
|
||||
table.original_abspath = os.path.abspath(path)
|
||||
return table
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def ref(self) -> Callable:
|
||||
"""The most important function in dbt is `ref()`; it's impossible to
|
||||
build even moderately complex models without it. `ref()` is how you
|
||||
@@ -913,11 +921,11 @@ class ProviderContext(ManifestContext):
|
||||
"""
|
||||
return self.provider.ref(self.db_wrapper, self.model, self.config, self.manifest)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def source(self) -> Callable:
|
||||
return self.provider.source(self.db_wrapper, self.model, self.config, self.manifest)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def metric(self) -> Callable:
|
||||
return self.provider.metric(self.db_wrapper, self.model, self.config, self.manifest)
|
||||
|
||||
@@ -978,7 +986,7 @@ class ProviderContext(ManifestContext):
|
||||
""" # noqa
|
||||
return self.provider.Config(self.model, self.context_config)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def execute(self) -> bool:
|
||||
"""`execute` is a Jinja variable that returns True when dbt is in
|
||||
"execute" mode.
|
||||
@@ -1039,7 +1047,7 @@ class ProviderContext(ManifestContext):
|
||||
""" # noqa
|
||||
return self.provider.execute
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def exceptions(self) -> Dict[str, Any]:
|
||||
"""The exceptions namespace can be used to raise warnings and errors in
|
||||
dbt userspace.
|
||||
@@ -1077,15 +1085,15 @@ class ProviderContext(ManifestContext):
|
||||
""" # noqa
|
||||
return wrapped_exports(self.model)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def database(self) -> str:
|
||||
return self.config.credentials.database
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def schema(self) -> str:
|
||||
return self.config.credentials.schema
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def var(self) -> ModelConfiguredVar:
|
||||
return self.provider.Var(
|
||||
context=self._ctx,
|
||||
@@ -1102,22 +1110,22 @@ class ProviderContext(ManifestContext):
|
||||
"""
|
||||
return self.db_wrapper
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def api(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"Relation": self.db_wrapper.Relation,
|
||||
"Column": self.adapter.Column,
|
||||
}
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def column(self) -> Type[Column]:
|
||||
return self.adapter.Column
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def env(self) -> Dict[str, Any]:
|
||||
return self.target
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def graph(self) -> Dict[str, Any]:
|
||||
"""The `graph` context variable contains information about the nodes in
|
||||
your dbt project. Models, sources, tests, and snapshots are all
|
||||
@@ -1226,30 +1234,42 @@ class ProviderContext(ManifestContext):
|
||||
|
||||
@contextproperty("model")
|
||||
def ctx_model(self) -> Dict[str, Any]:
|
||||
ret = self.model.to_dict(omit_none=True)
|
||||
model_dct = self.model.to_dict(omit_none=True)
|
||||
# Maintain direct use of compiled_sql
|
||||
# TODO add depreciation logic[CT-934]
|
||||
if "compiled_code" in ret:
|
||||
ret["compiled_sql"] = ret["compiled_code"]
|
||||
return ret
|
||||
if "compiled_code" in model_dct:
|
||||
model_dct["compiled_sql"] = model_dct["compiled_code"]
|
||||
|
||||
@contextproperty
|
||||
if (
|
||||
hasattr(self.model, "contract")
|
||||
and self.model.contract.alias_types is True
|
||||
and "columns" in model_dct
|
||||
):
|
||||
for column in model_dct["columns"].values():
|
||||
if "data_type" in column:
|
||||
orig_data_type = column["data_type"]
|
||||
# translate data_type to value in Column.TYPE_LABELS
|
||||
new_data_type = self.adapter.Column.translate_type(orig_data_type)
|
||||
column["data_type"] = new_data_type
|
||||
return model_dct
|
||||
|
||||
@contextproperty()
|
||||
def pre_hooks(self) -> Optional[List[Dict[str, Any]]]:
|
||||
return None
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def post_hooks(self) -> Optional[List[Dict[str, Any]]]:
|
||||
return None
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def sql(self) -> Optional[str]:
|
||||
return None
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def sql_now(self) -> str:
|
||||
return self.adapter.date_function()
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def adapter_macro(self, name: str, *args, **kwargs):
|
||||
"""This was deprecated in v0.18 in favor of adapter.dispatch"""
|
||||
msg = (
|
||||
@@ -1261,7 +1281,7 @@ class ProviderContext(ManifestContext):
|
||||
)
|
||||
raise CompilationError(msg)
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def env_var(self, var: str, default: Optional[str] = None) -> str:
|
||||
"""The env_var() function. Return the environment variable named 'var'.
|
||||
If there is no such environment variable set, return the default.
|
||||
@@ -1305,7 +1325,7 @@ class ProviderContext(ManifestContext):
|
||||
else:
|
||||
raise EnvVarMissingError(var)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def selected_resources(self) -> List[str]:
|
||||
"""The `selected_resources` variable contains a list of the resources
|
||||
selected based on the parameters provided to the dbt command.
|
||||
@@ -1314,7 +1334,7 @@ class ProviderContext(ManifestContext):
|
||||
"""
|
||||
return selected_resources.SELECTED_RESOURCES
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def submit_python_job(self, parsed_model: Dict, compiled_code: str) -> AdapterResponse:
|
||||
# Check macro_stack and that the unique id is for a materialization macro
|
||||
if not (
|
||||
@@ -1357,7 +1377,7 @@ class MacroContext(ProviderContext):
|
||||
class ModelContext(ProviderContext):
|
||||
model: ManifestNode
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def pre_hooks(self) -> List[Dict[str, Any]]:
|
||||
if self.model.resource_type in [NodeType.Source, NodeType.Test]:
|
||||
return []
|
||||
@@ -1366,7 +1386,7 @@ class ModelContext(ProviderContext):
|
||||
h.to_dict(omit_none=True) for h in self.model.config.pre_hook # type: ignore[union-attr] # noqa
|
||||
]
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def post_hooks(self) -> List[Dict[str, Any]]:
|
||||
if self.model.resource_type in [NodeType.Source, NodeType.Test]:
|
||||
return []
|
||||
@@ -1375,7 +1395,7 @@ class ModelContext(ProviderContext):
|
||||
h.to_dict(omit_none=True) for h in self.model.config.post_hook # type: ignore[union-attr] # noqa
|
||||
]
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def sql(self) -> Optional[str]:
|
||||
# only doing this in sql model for backward compatible
|
||||
if self.model.language == ModelLanguage.sql: # type: ignore[union-attr]
|
||||
@@ -1392,7 +1412,7 @@ class ModelContext(ProviderContext):
|
||||
else:
|
||||
return None
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def compiled_code(self) -> Optional[str]:
|
||||
if getattr(self.model, "defer_relation", None):
|
||||
# TODO https://github.com/dbt-labs/dbt-core/issues/7976
|
||||
@@ -1403,15 +1423,15 @@ class ModelContext(ProviderContext):
|
||||
else:
|
||||
return None
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def database(self) -> str:
|
||||
return getattr(self.model, "database", self.config.credentials.database)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def schema(self) -> str:
|
||||
return getattr(self.model, "schema", self.config.credentials.schema)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def this(self) -> Optional[RelationProxy]:
|
||||
"""`this` makes available schema information about the currently
|
||||
executing model. It's is useful in any context in which you need to
|
||||
@@ -1446,7 +1466,7 @@ class ModelContext(ProviderContext):
|
||||
return None
|
||||
return self.db_wrapper.Relation.create_from(self.config, self.model)
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def defer_relation(self) -> Optional[RelationProxy]:
|
||||
"""
|
||||
For commands which add information about this node's corresponding
|
||||
@@ -1660,7 +1680,7 @@ class TestContext(ProviderContext):
|
||||
)
|
||||
self.namespace = macro_namespace
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def env_var(self, var: str, default: Optional[str] = None) -> str:
|
||||
return_value = None
|
||||
if var.startswith(SECRET_ENV_PREFIX):
|
||||
|
||||
@@ -14,7 +14,7 @@ class SecretContext(BaseContext):
|
||||
"""This context is used in profiles.yml + packages.yml. It can render secret
|
||||
env vars that aren't usable elsewhere"""
|
||||
|
||||
@contextmember
|
||||
@contextmember()
|
||||
def env_var(self, var: str, default: Optional[str] = None) -> str:
|
||||
"""The env_var() function. Return the environment variable named 'var'.
|
||||
If there is no such environment variable set, return the default.
|
||||
|
||||
@@ -9,7 +9,7 @@ class TargetContext(BaseContext):
|
||||
super().__init__(cli_vars=cli_vars)
|
||||
self.target_dict = target_dict
|
||||
|
||||
@contextproperty
|
||||
@contextproperty()
|
||||
def target(self) -> Dict[str, Any]:
|
||||
"""`target` contains information about your connection to the warehouse
|
||||
(specified in profiles.yml). Some configs are shared between all
|
||||
|
||||
@@ -16,26 +16,21 @@ from dbt.utils import translate_aliases, md5
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import NewConnectionOpening
|
||||
from dbt.events.contextvars import get_node_info
|
||||
from typing_extensions import Protocol
|
||||
from typing_extensions import Protocol, Annotated
|
||||
from dbt.dataclass_schema import (
|
||||
dbtClassMixin,
|
||||
StrEnum,
|
||||
ExtensibleDbtClassMixin,
|
||||
HyphenatedDbtClassMixin,
|
||||
ValidatedStringMixin,
|
||||
register_pattern,
|
||||
)
|
||||
from dbt.contracts.util import Replaceable
|
||||
from mashumaro.jsonschema.annotations import Pattern
|
||||
|
||||
|
||||
class Identifier(ValidatedStringMixin):
|
||||
ValidationRegex = r"^[A-Za-z_][A-Za-z0-9_]+$"
|
||||
|
||||
|
||||
# we need register_pattern for jsonschema validation
|
||||
register_pattern(Identifier, r"^[A-Za-z_][A-Za-z0-9_]+$")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdapterResponse(dbtClassMixin):
|
||||
_message: str
|
||||
@@ -55,7 +50,8 @@ class ConnectionState(StrEnum):
|
||||
|
||||
@dataclass(init=False)
|
||||
class Connection(ExtensibleDbtClassMixin, Replaceable):
|
||||
type: Identifier
|
||||
# Annotated is used by mashumaro for jsonschema generation
|
||||
type: Annotated[Identifier, Pattern(r"^[A-Za-z_][A-Za-z0-9_]+$")]
|
||||
name: Optional[str] = None
|
||||
state: ConnectionState = ConnectionState.INIT
|
||||
transaction_open: bool = False
|
||||
@@ -108,7 +104,7 @@ class LazyHandle:
|
||||
connection, updating the handle on the Connection.
|
||||
"""
|
||||
|
||||
def __init__(self, opener: Callable[[Connection], Connection]):
|
||||
def __init__(self, opener: Callable[[Connection], Connection]) -> None:
|
||||
self.opener = opener
|
||||
|
||||
def resolve(self, connection: Connection) -> Connection:
|
||||
@@ -161,6 +157,7 @@ class Credentials(ExtensibleDbtClassMixin, Replaceable, metaclass=abc.ABCMeta):
|
||||
@classmethod
|
||||
def __pre_deserialize__(cls, data):
|
||||
data = super().__pre_deserialize__(data)
|
||||
# Need to fixup dbname => database, pass => password
|
||||
data = cls.translate_aliases(data)
|
||||
return data
|
||||
|
||||
@@ -220,10 +217,10 @@ DEFAULT_QUERY_COMMENT = """
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueryComment(HyphenatedDbtClassMixin):
|
||||
class QueryComment(dbtClassMixin):
|
||||
comment: str = DEFAULT_QUERY_COMMENT
|
||||
append: bool = False
|
||||
job_label: bool = False
|
||||
job_label: bool = field(default=False, metadata={"alias": "job-label"})
|
||||
|
||||
|
||||
class AdapterRequiredConfig(HasCredentials, Protocol):
|
||||
|
||||
@@ -225,10 +225,13 @@ class SchemaSourceFile(BaseSourceFile):
|
||||
sources: List[str] = field(default_factory=list)
|
||||
exposures: List[str] = field(default_factory=list)
|
||||
metrics: List[str] = field(default_factory=list)
|
||||
# metrics generated from semantic_model measures
|
||||
generated_metrics: List[str] = field(default_factory=list)
|
||||
groups: List[str] = field(default_factory=list)
|
||||
# node patches contain models, seeds, snapshots, analyses
|
||||
ndp: List[str] = field(default_factory=list)
|
||||
semantic_models: List[str] = field(default_factory=list)
|
||||
saved_queries: List[str] = field(default_factory=list)
|
||||
# any macro patches in this file by macro unique_id.
|
||||
mcp: Dict[str, str] = field(default_factory=dict)
|
||||
# any source patches in this file. The entries are package, name pairs
|
||||
|
||||
@@ -20,6 +20,7 @@ from typing import (
|
||||
Generic,
|
||||
AbstractSet,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
)
|
||||
from typing_extensions import Protocol
|
||||
from uuid import UUID
|
||||
@@ -37,6 +38,7 @@ from dbt.contracts.graph.nodes import (
|
||||
ModelNode,
|
||||
DeferRelation,
|
||||
ResultNode,
|
||||
SavedQuery,
|
||||
SemanticModel,
|
||||
SourceDefinition,
|
||||
UnpatchedSourceDefinition,
|
||||
@@ -44,7 +46,13 @@ from dbt.contracts.graph.nodes import (
|
||||
from dbt.contracts.graph.unparsed import SourcePatch, NodeVersion, UnparsedVersion
|
||||
from dbt.contracts.graph.manifest_upgrade import upgrade_manifest_json
|
||||
from dbt.contracts.files import SourceFile, SchemaSourceFile, FileHash, AnySourceFile
|
||||
from dbt.contracts.util import BaseArtifactMetadata, SourceKey, ArtifactMixin, schema_version
|
||||
from dbt.contracts.util import (
|
||||
BaseArtifactMetadata,
|
||||
SourceKey,
|
||||
ArtifactMixin,
|
||||
schema_version,
|
||||
get_artifact_schema_version,
|
||||
)
|
||||
from dbt.dataclass_schema import dbtClassMixin
|
||||
from dbt.exceptions import (
|
||||
CompilationError,
|
||||
@@ -88,7 +96,7 @@ def find_unique_id_for_package(storage, key, package: Optional[PackageName]):
|
||||
|
||||
|
||||
class DocLookup(dbtClassMixin):
|
||||
def __init__(self, manifest: "Manifest"):
|
||||
def __init__(self, manifest: "Manifest") -> None:
|
||||
self.storage: Dict[str, Dict[PackageName, UniqueID]] = {}
|
||||
self.populate(manifest)
|
||||
|
||||
@@ -119,7 +127,7 @@ class DocLookup(dbtClassMixin):
|
||||
|
||||
|
||||
class SourceLookup(dbtClassMixin):
|
||||
def __init__(self, manifest: "Manifest"):
|
||||
def __init__(self, manifest: "Manifest") -> None:
|
||||
self.storage: Dict[str, Dict[PackageName, UniqueID]] = {}
|
||||
self.populate(manifest)
|
||||
|
||||
@@ -156,7 +164,7 @@ class RefableLookup(dbtClassMixin):
|
||||
_lookup_types: ClassVar[set] = set(NodeType.refable())
|
||||
_versioned_types: ClassVar[set] = set(NodeType.versioned())
|
||||
|
||||
def __init__(self, manifest: "Manifest"):
|
||||
def __init__(self, manifest: "Manifest") -> None:
|
||||
self.storage: Dict[str, Dict[PackageName, UniqueID]] = {}
|
||||
self.populate(manifest)
|
||||
|
||||
@@ -267,7 +275,7 @@ class RefableLookup(dbtClassMixin):
|
||||
|
||||
|
||||
class MetricLookup(dbtClassMixin):
|
||||
def __init__(self, manifest: "Manifest"):
|
||||
def __init__(self, manifest: "Manifest") -> None:
|
||||
self.storage: Dict[str, Dict[PackageName, UniqueID]] = {}
|
||||
self.populate(manifest)
|
||||
|
||||
@@ -299,6 +307,41 @@ class MetricLookup(dbtClassMixin):
|
||||
return manifest.metrics[unique_id]
|
||||
|
||||
|
||||
class SavedQueryLookup(dbtClassMixin):
|
||||
"""Lookup utility for finding SavedQuery nodes"""
|
||||
|
||||
def __init__(self, manifest: "Manifest") -> None:
|
||||
self.storage: Dict[str, Dict[PackageName, UniqueID]] = {}
|
||||
self.populate(manifest)
|
||||
|
||||
def get_unique_id(self, search_name, package: Optional[PackageName]):
|
||||
return find_unique_id_for_package(self.storage, search_name, package)
|
||||
|
||||
def find(self, search_name, package: Optional[PackageName], manifest: "Manifest"):
|
||||
unique_id = self.get_unique_id(search_name, package)
|
||||
if unique_id is not None:
|
||||
return self.perform_lookup(unique_id, manifest)
|
||||
return None
|
||||
|
||||
def add_saved_query(self, saved_query: SavedQuery):
|
||||
if saved_query.search_name not in self.storage:
|
||||
self.storage[saved_query.search_name] = {}
|
||||
|
||||
self.storage[saved_query.search_name][saved_query.package_name] = saved_query.unique_id
|
||||
|
||||
def populate(self, manifest):
|
||||
for saved_query in manifest.saved_queries.values():
|
||||
if hasattr(saved_query, "name"):
|
||||
self.add_saved_query(saved_query)
|
||||
|
||||
def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> SavedQuery:
|
||||
if unique_id not in manifest.saved_queries:
|
||||
raise dbt.exceptions.DbtInternalError(
|
||||
f"SavedQUery {unique_id} found in cache but not found in manifest"
|
||||
)
|
||||
return manifest.saved_queries[unique_id]
|
||||
|
||||
|
||||
class SemanticModelByMeasureLookup(dbtClassMixin):
|
||||
"""Lookup utility for finding SemanticModel by measure
|
||||
|
||||
@@ -306,7 +349,7 @@ class SemanticModelByMeasureLookup(dbtClassMixin):
|
||||
the semantic models in a manifest.
|
||||
"""
|
||||
|
||||
def __init__(self, manifest: "Manifest"):
|
||||
def __init__(self, manifest: "Manifest") -> None:
|
||||
self.storage: DefaultDict[str, Dict[PackageName, UniqueID]] = defaultdict(dict)
|
||||
self.populate(manifest)
|
||||
|
||||
@@ -331,20 +374,31 @@ class SemanticModelByMeasureLookup(dbtClassMixin):
|
||||
"""Populate storage with all the measure + package paths to the Manifest's SemanticModels"""
|
||||
for semantic_model in manifest.semantic_models.values():
|
||||
self.add(semantic_model=semantic_model)
|
||||
for disabled in manifest.disabled.values():
|
||||
for node in disabled:
|
||||
if isinstance(node, SemanticModel):
|
||||
self.add(semantic_model=node)
|
||||
|
||||
def perform_lookup(self, unique_id: UniqueID, manifest: "Manifest") -> SemanticModel:
|
||||
"""Tries to get a SemanticModel from the Manifest"""
|
||||
semantic_model = manifest.semantic_models.get(unique_id)
|
||||
if semantic_model is None:
|
||||
enabled_semantic_model: Optional[SemanticModel] = manifest.semantic_models.get(unique_id)
|
||||
disabled_semantic_model: Optional[List] = manifest.disabled.get(unique_id)
|
||||
|
||||
if isinstance(enabled_semantic_model, SemanticModel):
|
||||
return enabled_semantic_model
|
||||
elif disabled_semantic_model is not None and isinstance(
|
||||
disabled_semantic_model[0], SemanticModel
|
||||
):
|
||||
return disabled_semantic_model[0]
|
||||
else:
|
||||
raise dbt.exceptions.DbtInternalError(
|
||||
f"Semantic model `{unique_id}` found in cache but not found in manifest"
|
||||
)
|
||||
return semantic_model
|
||||
|
||||
|
||||
# This handles both models/seeds/snapshots and sources/metrics/exposures
|
||||
# This handles both models/seeds/snapshots and sources/metrics/exposures/semantic_models
|
||||
class DisabledLookup(dbtClassMixin):
|
||||
def __init__(self, manifest: "Manifest"):
|
||||
def __init__(self, manifest: "Manifest") -> None:
|
||||
self.storage: Dict[str, Dict[PackageName, List[Any]]] = {}
|
||||
self.populate(manifest)
|
||||
|
||||
@@ -598,6 +652,9 @@ class Disabled(Generic[D]):
|
||||
MaybeMetricNode = Optional[Union[Metric, Disabled[Metric]]]
|
||||
|
||||
|
||||
MaybeSavedQueryNode = Optional[Union[SavedQuery, Disabled[SavedQuery]]]
|
||||
|
||||
|
||||
MaybeDocumentation = Optional[Documentation]
|
||||
|
||||
|
||||
@@ -742,6 +799,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
disabled: MutableMapping[str, List[GraphMemberNode]] = field(default_factory=dict)
|
||||
env_vars: MutableMapping[str, str] = field(default_factory=dict)
|
||||
semantic_models: MutableMapping[str, SemanticModel] = field(default_factory=dict)
|
||||
saved_queries: MutableMapping[str, SavedQuery] = field(default_factory=dict)
|
||||
|
||||
_doc_lookup: Optional[DocLookup] = field(
|
||||
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
|
||||
@@ -755,6 +813,9 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
_metric_lookup: Optional[MetricLookup] = field(
|
||||
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
|
||||
)
|
||||
_saved_query_lookup: Optional[SavedQueryLookup] = field(
|
||||
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
|
||||
)
|
||||
_semantic_model_by_measure_lookup: Optional[SemanticModelByMeasureLookup] = field(
|
||||
default=None, metadata={"serialize": lambda x: None, "deserialize": lambda x: None}
|
||||
)
|
||||
@@ -799,6 +860,9 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
"semantic_models": {
|
||||
k: v.to_dict(omit_none=False) for k, v in self.semantic_models.items()
|
||||
},
|
||||
"saved_queries": {
|
||||
k: v.to_dict(omit_none=False) for k, v in self.saved_queries.items()
|
||||
},
|
||||
}
|
||||
|
||||
def build_disabled_by_file_id(self):
|
||||
@@ -860,6 +924,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
self.sources.values(),
|
||||
self.metrics.values(),
|
||||
self.semantic_models.values(),
|
||||
self.saved_queries.values(),
|
||||
)
|
||||
for resource in all_resources:
|
||||
resource_type_plural = resource.resource_type.pluralize()
|
||||
@@ -895,6 +960,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
files={k: _deepcopy(v) for k, v in self.files.items()},
|
||||
state_check=_deepcopy(self.state_check),
|
||||
semantic_models={k: _deepcopy(v) for k, v in self.semantic_models.items()},
|
||||
saved_queries={k: _deepcopy(v) for k, v in self.saved_queries.items()},
|
||||
)
|
||||
copy.build_flat_graph()
|
||||
return copy
|
||||
@@ -907,6 +973,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
self.exposures.values(),
|
||||
self.metrics.values(),
|
||||
self.semantic_models.values(),
|
||||
self.saved_queries.values(),
|
||||
)
|
||||
)
|
||||
forward_edges, backward_edges = build_node_edges(edge_members)
|
||||
@@ -927,13 +994,22 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
groupable_nodes = list(
|
||||
chain(
|
||||
self.nodes.values(),
|
||||
self.saved_queries.values(),
|
||||
self.semantic_models.values(),
|
||||
self.metrics.values(),
|
||||
)
|
||||
)
|
||||
group_map = {group.name: [] for group in self.groups.values()}
|
||||
for node in groupable_nodes:
|
||||
if node.group is not None:
|
||||
group_map[node.group].append(node.unique_id)
|
||||
# group updates are not included with state:modified and
|
||||
# by ignoring the groups that aren't in the group map we
|
||||
# can avoid hitting errors for groups that are not getting
|
||||
# updated. This is a hack but any groups that are not
|
||||
# valid will be caught in
|
||||
# parser.manifest.ManifestLoader.check_valid_group_config_node
|
||||
if node.group in group_map:
|
||||
group_map[node.group].append(node.unique_id)
|
||||
self.group_map = group_map
|
||||
|
||||
def writable_manifest(self) -> "WritableManifest":
|
||||
@@ -954,6 +1030,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
parent_map=self.parent_map,
|
||||
group_map=self.group_map,
|
||||
semantic_models=self.semantic_models,
|
||||
saved_queries=self.saved_queries,
|
||||
)
|
||||
|
||||
def write(self, path):
|
||||
@@ -972,6 +1049,8 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
return self.metrics[unique_id]
|
||||
elif unique_id in self.semantic_models:
|
||||
return self.semantic_models[unique_id]
|
||||
elif unique_id in self.saved_queries:
|
||||
return self.saved_queries[unique_id]
|
||||
else:
|
||||
# something terrible has happened
|
||||
raise dbt.exceptions.DbtInternalError(
|
||||
@@ -1008,6 +1087,13 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
self._metric_lookup = MetricLookup(self)
|
||||
return self._metric_lookup
|
||||
|
||||
@property
|
||||
def saved_query_lookup(self) -> SavedQueryLookup:
|
||||
"""Retuns a SavedQueryLookup, instantiating it first if necessary."""
|
||||
if self._saved_query_lookup is None:
|
||||
self._saved_query_lookup = SavedQueryLookup(self)
|
||||
return self._saved_query_lookup
|
||||
|
||||
@property
|
||||
def semantic_model_by_measure_lookup(self) -> SemanticModelByMeasureLookup:
|
||||
"""Gets (and creates if necessary) the lookup utility for getting SemanticModels by measures"""
|
||||
@@ -1056,8 +1142,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
|
||||
return resolved_refs
|
||||
|
||||
# Called by dbt.parser.manifest._process_refs_for_exposure, _process_refs_for_metric,
|
||||
# and dbt.parser.manifest._process_refs_for_node
|
||||
# Called by dbt.parser.manifest._process_refs & ManifestLoader.check_for_model_deprecations
|
||||
def resolve_ref(
|
||||
self,
|
||||
source_node: GraphMemberNode,
|
||||
@@ -1142,6 +1227,35 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
return Disabled(disabled[0])
|
||||
return None
|
||||
|
||||
def resolve_saved_query(
|
||||
self,
|
||||
target_saved_query_name: str,
|
||||
target_saved_query_package: Optional[str],
|
||||
current_project: str,
|
||||
node_package: str,
|
||||
) -> MaybeSavedQueryNode:
|
||||
"""Tries to find the SavedQuery by name within the available project and packages.
|
||||
|
||||
Will return the first enabled SavedQuery matching the name found while iterating over
|
||||
the scoped packages. If no enabled SavedQuery node match is found, returns the last
|
||||
disabled SavedQuery node. Otherwise it returns None.
|
||||
"""
|
||||
disabled: Optional[List[SavedQuery]] = None
|
||||
candidates = _packages_to_search(current_project, node_package, target_saved_query_package)
|
||||
for pkg in candidates:
|
||||
saved_query = self.saved_query_lookup.find(target_saved_query_name, pkg, self)
|
||||
|
||||
if saved_query is not None and saved_query.config.enabled:
|
||||
return saved_query
|
||||
|
||||
# it's possible that the node is disabled
|
||||
if disabled is None:
|
||||
disabled = self.disabled_lookup.find(f"{target_saved_query_name}", pkg)
|
||||
if disabled:
|
||||
return Disabled(disabled[0])
|
||||
|
||||
return None
|
||||
|
||||
def resolve_semantic_model_for_measure(
|
||||
self,
|
||||
target_measure_name: str,
|
||||
@@ -1156,6 +1270,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
semantic_model = self.semantic_model_by_measure_lookup.find(
|
||||
target_measure_name, pkg, self
|
||||
)
|
||||
# need to return it even if it's disabled so know it's not fully missing
|
||||
if semantic_model is not None:
|
||||
return semantic_model
|
||||
|
||||
@@ -1331,10 +1446,13 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
self.exposures[exposure.unique_id] = exposure
|
||||
source_file.exposures.append(exposure.unique_id)
|
||||
|
||||
def add_metric(self, source_file: SchemaSourceFile, metric: Metric):
|
||||
def add_metric(self, source_file: SchemaSourceFile, metric: Metric, generated: bool = False):
|
||||
_check_duplicates(metric, self.metrics)
|
||||
self.metrics[metric.unique_id] = metric
|
||||
source_file.metrics.append(metric.unique_id)
|
||||
if not generated:
|
||||
source_file.metrics.append(metric.unique_id)
|
||||
else:
|
||||
source_file.generated_metrics.append(metric.unique_id)
|
||||
|
||||
def add_group(self, source_file: SchemaSourceFile, group: Group):
|
||||
_check_duplicates(group, self.groups)
|
||||
@@ -1356,6 +1474,10 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
source_file.add_test(node.unique_id, test_from)
|
||||
if isinstance(node, Metric):
|
||||
source_file.metrics.append(node.unique_id)
|
||||
if isinstance(node, SavedQuery):
|
||||
source_file.saved_queries.append(node.unique_id)
|
||||
if isinstance(node, SemanticModel):
|
||||
source_file.semantic_models.append(node.unique_id)
|
||||
if isinstance(node, Exposure):
|
||||
source_file.exposures.append(node.unique_id)
|
||||
else:
|
||||
@@ -1371,6 +1493,11 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
self.semantic_models[semantic_model.unique_id] = semantic_model
|
||||
source_file.semantic_models.append(semantic_model.unique_id)
|
||||
|
||||
def add_saved_query(self, source_file: SchemaSourceFile, saved_query: SavedQuery) -> None:
|
||||
_check_duplicates(saved_query, self.saved_queries)
|
||||
self.saved_queries[saved_query.unique_id] = saved_query
|
||||
source_file.saved_queries.append(saved_query.unique_id)
|
||||
|
||||
# end of methods formerly in ParseResult
|
||||
|
||||
# Provide support for copy.deepcopy() - we just need to avoid the lock!
|
||||
@@ -1398,6 +1525,7 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
self.disabled,
|
||||
self.env_vars,
|
||||
self.semantic_models,
|
||||
self.saved_queries,
|
||||
self._doc_lookup,
|
||||
self._source_lookup,
|
||||
self._ref_lookup,
|
||||
@@ -1410,19 +1538,19 @@ class Manifest(MacroMethods, DataClassMessagePackMixin, dbtClassMixin):
|
||||
|
||||
|
||||
class MacroManifest(MacroMethods):
|
||||
def __init__(self, macros):
|
||||
def __init__(self, macros) -> None:
|
||||
self.macros = macros
|
||||
self.metadata = ManifestMetadata()
|
||||
# This is returned by the 'graph' context property
|
||||
# in the ProviderContext class.
|
||||
self.flat_graph = {}
|
||||
self.flat_graph: Dict[str, Any] = {}
|
||||
|
||||
|
||||
AnyManifest = Union[Manifest, MacroManifest]
|
||||
|
||||
|
||||
@dataclass
|
||||
@schema_version("manifest", 10)
|
||||
@schema_version("manifest", 11)
|
||||
class WritableManifest(ArtifactMixin):
|
||||
nodes: Mapping[UniqueID, ManifestNode] = field(
|
||||
metadata=dict(description=("The nodes defined in the dbt project and its dependencies"))
|
||||
@@ -1468,6 +1596,9 @@ class WritableManifest(ArtifactMixin):
|
||||
description="A mapping from group names to their nodes",
|
||||
)
|
||||
)
|
||||
saved_queries: Mapping[UniqueID, SavedQuery] = field(
|
||||
metadata=dict(description=("The saved queries defined in the dbt project"))
|
||||
)
|
||||
semantic_models: Mapping[UniqueID, SemanticModel] = field(
|
||||
metadata=dict(description=("The semantic models defined in the dbt project"))
|
||||
)
|
||||
@@ -1478,7 +1609,7 @@ class WritableManifest(ArtifactMixin):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def compatible_previous_versions(self):
|
||||
def compatible_previous_versions(cls) -> Iterable[Tuple[str, int]]:
|
||||
return [
|
||||
("manifest", 4),
|
||||
("manifest", 5),
|
||||
@@ -1486,14 +1617,15 @@ class WritableManifest(ArtifactMixin):
|
||||
("manifest", 7),
|
||||
("manifest", 8),
|
||||
("manifest", 9),
|
||||
("manifest", 10),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def upgrade_schema_version(cls, data):
|
||||
"""This overrides the "upgrade_schema_version" call in VersionedSchema (via
|
||||
ArtifactMixin) to modify the dictionary passed in from earlier versions of the manifest."""
|
||||
manifest_schema_version = get_manifest_schema_version(data)
|
||||
if manifest_schema_version <= 9:
|
||||
manifest_schema_version = get_artifact_schema_version(data)
|
||||
if manifest_schema_version <= 10:
|
||||
data = upgrade_manifest_json(data, manifest_schema_version)
|
||||
return cls.from_dict(data)
|
||||
|
||||
@@ -1506,13 +1638,6 @@ class WritableManifest(ArtifactMixin):
|
||||
return dct
|
||||
|
||||
|
||||
def get_manifest_schema_version(dct: dict) -> int:
|
||||
schema_version = dct.get("metadata", {}).get("dbt_schema_version", None)
|
||||
if not schema_version:
|
||||
raise ValueError("Manifest doesn't have schema version")
|
||||
return int(schema_version.split(".")[-2][-1])
|
||||
|
||||
|
||||
def _check_duplicates(value: BaseNode, src: Mapping[str, BaseNode]):
|
||||
if value.unique_id in src:
|
||||
raise DuplicateResourceNameError(value, src[value.unique_id])
|
||||
|
||||
@@ -62,10 +62,72 @@ def drop_v9_and_prior_metrics(manifest: dict) -> None:
|
||||
manifest["disabled"] = filtered_disabled_entries
|
||||
|
||||
|
||||
def _convert_dct_with_filter(v10_dct_with_opt_filter):
|
||||
"""Upgrage the filter object from v10 to v11.
|
||||
|
||||
v10 filters from a serialized manifest looked like:
|
||||
{..., 'filter': {'where_sql_template': '<filter_value>'}}
|
||||
whereas v11 filters look like:
|
||||
{..., 'filter': {'where_filters': [{'where_sql_template': '<filter_value>'}, ...]}}
|
||||
"""
|
||||
if v10_dct_with_opt_filter is not None and v10_dct_with_opt_filter.get("filter") is not None:
|
||||
v10_dct_with_opt_filter["filter"] = {"where_filters": [v10_dct_with_opt_filter["filter"]]}
|
||||
|
||||
|
||||
def _convert_metric(v10_metric_dict):
|
||||
"""Upgrades a v10 metric object to a v11 metric object.
|
||||
|
||||
Specifcally the following properties change
|
||||
1. metric.filter
|
||||
2. metric.type_params.measure.filter
|
||||
3. metric.type_params.input_measures[x].filter
|
||||
4. metric.type_params.numerator.filter
|
||||
5. metric.type_params.denominator.filter
|
||||
6. metric.type_params.metrics[x].filter"
|
||||
"""
|
||||
|
||||
# handles top level metric filter
|
||||
_convert_dct_with_filter(v10_metric_dict)
|
||||
|
||||
type_params = v10_metric_dict.get("type_params")
|
||||
if type_params is not None:
|
||||
_convert_dct_with_filter(type_params.get("measure"))
|
||||
_convert_dct_with_filter(type_params.get("numerator"))
|
||||
_convert_dct_with_filter(type_params.get("denominator"))
|
||||
|
||||
# handles metric.type_params.input_measures[x].filter
|
||||
input_measures = type_params.get("input_measures")
|
||||
if input_measures is not None:
|
||||
for input_measure in input_measures:
|
||||
_convert_dct_with_filter(input_measure)
|
||||
|
||||
# handles metric.type_params.metrics[x].filter
|
||||
metrics = type_params.get("metrics")
|
||||
if metrics is not None:
|
||||
for metric in metrics:
|
||||
_convert_dct_with_filter(metric)
|
||||
|
||||
|
||||
def upgrade_v10_metric_filters(manifest: dict):
|
||||
"""Handles metric filters changes from v10 to v11."""
|
||||
|
||||
metrics = manifest.get("metrics", {})
|
||||
for metric in metrics.values():
|
||||
_convert_metric(metric)
|
||||
|
||||
disabled_nodes = manifest.get("disabled", {})
|
||||
for unique_id, nodes in disabled_nodes.items():
|
||||
if unique_id.split(".")[0] == "metric":
|
||||
for node in nodes:
|
||||
_convert_metric(node)
|
||||
|
||||
|
||||
def upgrade_manifest_json(manifest: dict, manifest_schema_version: int) -> dict:
|
||||
# this should remain 9 while the check in `upgrade_schema_version` may change
|
||||
if manifest_schema_version <= 9:
|
||||
drop_v9_and_prior_metrics(manifest=manifest)
|
||||
elif manifest_schema_version == 10:
|
||||
upgrade_v10_metric_filters(manifest=manifest)
|
||||
|
||||
for node_content in manifest.get("nodes", {}).values():
|
||||
upgrade_node_content(node_content)
|
||||
@@ -104,4 +166,6 @@ def upgrade_manifest_json(manifest: dict, manifest_schema_version: int) -> dict:
|
||||
doc_content["resource_type"] = "doc"
|
||||
if "semantic_models" not in manifest:
|
||||
manifest["semantic_models"] = {}
|
||||
if "saved_queries" not in manifest:
|
||||
manifest["saved_queries"] = {}
|
||||
return manifest
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.contracts.graph.manifest import Manifest, Metric
|
||||
from dbt_semantic_interfaces.type_enums import MetricType
|
||||
|
||||
from typing import Any, Dict, Iterator, List
|
||||
|
||||
|
||||
DERIVED_METRICS = [MetricType.DERIVED, MetricType.RATIO]
|
||||
BASE_METRICS = [MetricType.SIMPLE, MetricType.CUMULATIVE, MetricType.CONVERSION]
|
||||
|
||||
|
||||
class MetricReference(object):
|
||||
def __init__(self, metric_name, package_name=None):
|
||||
def __init__(self, metric_name, package_name=None) -> None:
|
||||
self.metric_name = metric_name
|
||||
self.package_name = package_name
|
||||
|
||||
@@ -17,76 +24,74 @@ class ResolvedMetricReference(MetricReference):
|
||||
for working with metrics (ie. __str__ and templating functions)
|
||||
"""
|
||||
|
||||
def __init__(self, node, manifest, Relation):
|
||||
def __init__(self, node: Metric, manifest: Manifest) -> None:
|
||||
super().__init__(node.name, node.package_name)
|
||||
self.node = node
|
||||
self.manifest = manifest
|
||||
self.Relation = Relation
|
||||
|
||||
def __getattr__(self, key):
|
||||
def __getattr__(self, key) -> Any:
|
||||
return getattr(self.node, key)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.node.name}"
|
||||
|
||||
@classmethod
|
||||
def parent_metrics(cls, metric_node, manifest):
|
||||
def parent_metrics(cls, metric_node: Metric, manifest: Manifest) -> Iterator[Metric]:
|
||||
"""For a given metric, yeilds all upstream metrics."""
|
||||
yield metric_node
|
||||
|
||||
for parent_unique_id in metric_node.depends_on.nodes:
|
||||
node = manifest.metrics.get(parent_unique_id)
|
||||
if node and node.resource_type == NodeType.Metric:
|
||||
node = manifest.expect(parent_unique_id)
|
||||
if isinstance(node, Metric):
|
||||
yield from cls.parent_metrics(node, manifest)
|
||||
|
||||
@classmethod
|
||||
def parent_metrics_names(cls, metric_node, manifest):
|
||||
yield metric_node.name
|
||||
|
||||
for parent_unique_id in metric_node.depends_on.nodes:
|
||||
node = manifest.metrics.get(parent_unique_id)
|
||||
if node and node.resource_type == NodeType.Metric:
|
||||
yield from cls.parent_metrics_names(node, manifest)
|
||||
def parent_metrics_names(cls, metric_node: Metric, manifest: Manifest) -> Iterator[str]:
|
||||
"""For a given metric, yeilds all upstream metric names"""
|
||||
for metric in cls.parent_metrics(metric_node, manifest):
|
||||
yield metric.name
|
||||
|
||||
@classmethod
|
||||
def reverse_dag_parsing(cls, metric_node, manifest, metric_depth_count):
|
||||
if metric_node.calculation_method == "derived":
|
||||
yield {metric_node.name: metric_depth_count}
|
||||
metric_depth_count = metric_depth_count + 1
|
||||
def reverse_dag_parsing(
|
||||
cls, metric_node: Metric, manifest: Manifest, metric_depth_count: int
|
||||
) -> Iterator[Dict[str, int]]:
|
||||
"""For the given metric, yeilds dictionaries having {<metric_name>: <depth_from_initial_metric} of upstream derived metrics.
|
||||
|
||||
for parent_unique_id in metric_node.depends_on.nodes:
|
||||
node = manifest.metrics.get(parent_unique_id)
|
||||
if (
|
||||
node
|
||||
and node.resource_type == NodeType.Metric
|
||||
and node.calculation_method == "derived"
|
||||
):
|
||||
yield from cls.reverse_dag_parsing(node, manifest, metric_depth_count)
|
||||
This function is intended as a helper function for other metric helper functions.
|
||||
"""
|
||||
if metric_node.type in DERIVED_METRICS:
|
||||
yield {metric_node.name: metric_depth_count}
|
||||
|
||||
for parent_unique_id in metric_node.depends_on.nodes:
|
||||
node = manifest.expect(parent_unique_id)
|
||||
if isinstance(node, Metric):
|
||||
yield from cls.reverse_dag_parsing(node, manifest, metric_depth_count + 1)
|
||||
|
||||
def full_metric_dependency(self):
|
||||
"""Returns a unique list of all upstream metric names."""
|
||||
to_return = list(set(self.parent_metrics_names(self.node, self.manifest)))
|
||||
return to_return
|
||||
|
||||
def base_metric_dependency(self):
|
||||
def base_metric_dependency(self) -> List[str]:
|
||||
"""Returns a unique list of names for all upstream non-derived metrics."""
|
||||
in_scope_metrics = list(self.parent_metrics(self.node, self.manifest))
|
||||
base_metrics = {
|
||||
metric.name for metric in in_scope_metrics if metric.type not in DERIVED_METRICS
|
||||
}
|
||||
|
||||
to_return = []
|
||||
for metric in in_scope_metrics:
|
||||
if metric.calculation_method != "derived" and metric.name not in to_return:
|
||||
to_return.append(metric.name)
|
||||
return list(base_metrics)
|
||||
|
||||
return to_return
|
||||
|
||||
def derived_metric_dependency(self):
|
||||
def derived_metric_dependency(self) -> List[str]:
|
||||
"""Returns a unique list of names for all upstream derived metrics."""
|
||||
in_scope_metrics = list(self.parent_metrics(self.node, self.manifest))
|
||||
derived_metrics = {
|
||||
metric.name for metric in in_scope_metrics if metric.type in DERIVED_METRICS
|
||||
}
|
||||
|
||||
to_return = []
|
||||
for metric in in_scope_metrics:
|
||||
if metric.calculation_method == "derived" and metric.name not in to_return:
|
||||
to_return.append(metric.name)
|
||||
return list(derived_metrics)
|
||||
|
||||
return to_return
|
||||
|
||||
def derived_metric_dependency_depth(self):
|
||||
def derived_metric_dependency_depth(self) -> List[Dict[str, int]]:
|
||||
"""Returns a list of {<metric_name>: <depth_from_initial_metric>} for all upstream metrics."""
|
||||
metric_depth_count = 1
|
||||
to_return = list(self.reverse_dag_parsing(self.node, self.manifest, metric_depth_count))
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ from dataclasses import field, Field, dataclass
|
||||
from enum import Enum
|
||||
from itertools import chain
|
||||
from typing import Any, List, Optional, Dict, Union, Type, TypeVar, Callable
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from dbt.dataclass_schema import (
|
||||
dbtClassMixin,
|
||||
ValidationError,
|
||||
register_pattern,
|
||||
StrEnum,
|
||||
)
|
||||
from dbt.contracts.graph.unparsed import AdditionalPropertiesAllowed, Docs
|
||||
@@ -14,7 +14,9 @@ from dbt.contracts.graph.utils import validate_color
|
||||
from dbt.contracts.util import Replaceable, list_str
|
||||
from dbt.exceptions import DbtInternalError, CompilationError
|
||||
from dbt import hooks
|
||||
from dbt.node_types import NodeType
|
||||
from dbt.node_types import NodeType, AccessType
|
||||
from dbt_semantic_interfaces.type_enums.export_destination_type import ExportDestinationType
|
||||
from mashumaro.jsonschema.annotations import Pattern
|
||||
|
||||
|
||||
M = TypeVar("M", bound="Metadata")
|
||||
@@ -188,9 +190,6 @@ class Severity(str):
|
||||
pass
|
||||
|
||||
|
||||
register_pattern(Severity, insensitive_patterns("warn", "error"))
|
||||
|
||||
|
||||
class OnConfigurationChangeOption(StrEnum):
|
||||
Apply = "apply"
|
||||
Continue = "continue"
|
||||
@@ -204,6 +203,7 @@ class OnConfigurationChangeOption(StrEnum):
|
||||
@dataclass
|
||||
class ContractConfig(dbtClassMixin, Replaceable):
|
||||
enforced: bool = False
|
||||
alias_types: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -218,7 +218,6 @@ T = TypeVar("T", bound="BaseConfig")
|
||||
|
||||
@dataclass
|
||||
class BaseConfig(AdditionalPropertiesAllowed, Replaceable):
|
||||
|
||||
# enable syntax like: config['key']
|
||||
def __getitem__(self, key):
|
||||
return self.get(key)
|
||||
@@ -376,25 +375,50 @@ class BaseConfig(AdditionalPropertiesAllowed, Replaceable):
|
||||
self.validate(dct)
|
||||
return self.from_dict(dct)
|
||||
|
||||
def replace(self, **kwargs):
|
||||
dct = self.to_dict(omit_none=True)
|
||||
|
||||
mapping = self.field_mapping()
|
||||
for key, value in kwargs.items():
|
||||
new_key = mapping.get(key, key)
|
||||
dct[new_key] = value
|
||||
return self.from_dict(dct)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SemanticModelConfig(BaseConfig):
|
||||
enabled: bool = True
|
||||
group: Optional[str] = field(
|
||||
default=None,
|
||||
metadata=CompareBehavior.Exclude.meta(),
|
||||
)
|
||||
meta: Dict[str, Any] = field(
|
||||
default_factory=dict,
|
||||
metadata=MergeBehavior.Update.meta(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SavedQueryConfig(BaseConfig):
|
||||
"""Where config options for SavedQueries are stored.
|
||||
|
||||
This class is much like many other node config classes. It's likely that
|
||||
this class will expand in the direction of what's in the `NodeAndTestConfig`
|
||||
class. It might make sense to clean the various *Config classes into one at
|
||||
some point.
|
||||
"""
|
||||
|
||||
enabled: bool = True
|
||||
group: Optional[str] = field(
|
||||
default=None,
|
||||
metadata=CompareBehavior.Exclude.meta(),
|
||||
)
|
||||
meta: Dict[str, Any] = field(
|
||||
default_factory=dict,
|
||||
metadata=MergeBehavior.Update.meta(),
|
||||
)
|
||||
export_as: Optional[ExportDestinationType] = None
|
||||
schema: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetricConfig(BaseConfig):
|
||||
enabled: bool = True
|
||||
group: Optional[str] = None
|
||||
group: Optional[str] = field(
|
||||
default=None,
|
||||
metadata=CompareBehavior.Exclude.meta(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -447,11 +471,11 @@ class NodeConfig(NodeAndTestConfig):
|
||||
persist_docs: Dict[str, Any] = field(default_factory=dict)
|
||||
post_hook: List[Hook] = field(
|
||||
default_factory=list,
|
||||
metadata=MergeBehavior.Append.meta(),
|
||||
metadata={"merge": MergeBehavior.Append, "alias": "post-hook"},
|
||||
)
|
||||
pre_hook: List[Hook] = field(
|
||||
default_factory=list,
|
||||
metadata=MergeBehavior.Append.meta(),
|
||||
metadata={"merge": MergeBehavior.Append, "alias": "pre-hook"},
|
||||
)
|
||||
quoting: Dict[str, Any] = field(
|
||||
default_factory=dict,
|
||||
@@ -511,39 +535,29 @@ class NodeConfig(NodeAndTestConfig):
|
||||
@classmethod
|
||||
def __pre_deserialize__(cls, data):
|
||||
data = super().__pre_deserialize__(data)
|
||||
field_map = {"post-hook": "post_hook", "pre-hook": "pre_hook"}
|
||||
# create a new dict because otherwise it gets overwritten in
|
||||
# tests
|
||||
new_dict = {}
|
||||
for key in data:
|
||||
new_dict[key] = data[key]
|
||||
data = new_dict
|
||||
for key in hooks.ModelHookType:
|
||||
if key in data:
|
||||
data[key] = [hooks.get_hook_dict(h) for h in data[key]]
|
||||
for field_name in field_map:
|
||||
if field_name in data:
|
||||
new_name = field_map[field_name]
|
||||
data[new_name] = data.pop(field_name)
|
||||
return data
|
||||
|
||||
def __post_serialize__(self, dct):
|
||||
dct = super().__post_serialize__(dct)
|
||||
field_map = {"post_hook": "post-hook", "pre_hook": "pre-hook"}
|
||||
for field_name in field_map:
|
||||
if field_name in dct:
|
||||
dct[field_map[field_name]] = dct.pop(field_name)
|
||||
return dct
|
||||
|
||||
# this is still used by jsonschema validation
|
||||
@classmethod
|
||||
def field_mapping(cls):
|
||||
return {"post_hook": "post-hook", "pre_hook": "pre-hook"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelConfig(NodeConfig):
|
||||
access: AccessType = field(
|
||||
default=AccessType.Protected,
|
||||
metadata=MergeBehavior.Update.meta(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SeedConfig(NodeConfig):
|
||||
materialized: str = "seed"
|
||||
delimiter: str = ","
|
||||
quote_columns: Optional[bool] = None
|
||||
|
||||
@classmethod
|
||||
@@ -553,6 +567,9 @@ class SeedConfig(NodeConfig):
|
||||
raise ValidationError("A seed must have a materialized value of 'seed'")
|
||||
|
||||
|
||||
SEVERITY_PATTERN = r"^([Ww][Aa][Rr][Nn]|[Ee][Rr][Rr][Oo][Rr])$"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestConfig(NodeAndTestConfig):
|
||||
__test__ = False
|
||||
@@ -563,14 +580,64 @@ class TestConfig(NodeAndTestConfig):
|
||||
metadata=CompareBehavior.Exclude.meta(),
|
||||
)
|
||||
materialized: str = "test"
|
||||
severity: Severity = Severity("ERROR")
|
||||
# Annotated is used by mashumaro for jsonschema generation
|
||||
severity: Annotated[Severity, Pattern(SEVERITY_PATTERN)] = Severity("ERROR")
|
||||
store_failures: Optional[bool] = None
|
||||
store_failures_as: Optional[str] = None
|
||||
where: Optional[str] = None
|
||||
limit: Optional[int] = None
|
||||
fail_calc: str = "count(*)"
|
||||
warn_if: str = "!= 0"
|
||||
error_if: str = "!= 0"
|
||||
|
||||
def __post_init__(self):
|
||||
"""
|
||||
The presence of a setting for `store_failures_as` overrides any existing setting for `store_failures`,
|
||||
regardless of level of granularity. If `store_failures_as` is not set, then `store_failures` takes effect.
|
||||
At the time of implementation, `store_failures = True` would always create a table; the user could not
|
||||
configure this. Hence, if `store_failures = True` and `store_failures_as` is not specified, then it
|
||||
should be set to "table" to mimic the existing functionality.
|
||||
|
||||
A side effect of this overriding functionality is that `store_failures_as="view"` at the project
|
||||
level cannot be turned off at the model level without setting both `store_failures_as` and
|
||||
`store_failures`. The former would cascade down and override `store_failures=False`. The proposal
|
||||
is to include "ephemeral" as a value for `store_failures_as`, which effectively sets
|
||||
`store_failures=False`.
|
||||
|
||||
The exception handling for this is tricky. If we raise an exception here, the entire run fails at
|
||||
parse time. We would rather well-formed models run successfully, leaving only exceptions to be rerun
|
||||
if necessary. Hence, the exception needs to be raised in the test materialization. In order to do so,
|
||||
we need to make sure that we go down the `store_failures = True` route with the invalid setting for
|
||||
`store_failures_as`. This results in the `.get()` defaulted to `True` below, instead of a normal
|
||||
dictionary lookup as is done in the `if` block. Refer to the test materialization for the
|
||||
exception that is raise as a result of an invalid value.
|
||||
|
||||
The intention of this block is to behave as if `store_failures_as` is the only setting,
|
||||
but still allow for backwards compatibility for `store_failures`.
|
||||
See https://github.com/dbt-labs/dbt-core/issues/6914 for more information.
|
||||
"""
|
||||
|
||||
# if `store_failures_as` is not set, it gets set by `store_failures`
|
||||
# the settings below mimic existing behavior prior to `store_failures_as`
|
||||
get_store_failures_as_map = {
|
||||
True: "table",
|
||||
False: "ephemeral",
|
||||
None: None,
|
||||
}
|
||||
|
||||
# if `store_failures_as` is set, it dictates what `store_failures` gets set to
|
||||
# the settings below overrides whatever `store_failures` is set to by the user
|
||||
get_store_failures_map = {
|
||||
"ephemeral": False,
|
||||
"table": True,
|
||||
"view": True,
|
||||
}
|
||||
|
||||
if self.store_failures_as is None:
|
||||
self.store_failures_as = get_store_failures_as_map[self.store_failures]
|
||||
else:
|
||||
self.store_failures = get_store_failures_map.get(self.store_failures_as, True)
|
||||
|
||||
@classmethod
|
||||
def same_contents(cls, unrendered: Dict[str, Any], other: Dict[str, Any]) -> bool:
|
||||
"""This is like __eq__, except it explicitly checks certain fields."""
|
||||
@@ -582,6 +649,7 @@ class TestConfig(NodeAndTestConfig):
|
||||
"warn_if",
|
||||
"error_if",
|
||||
"store_failures",
|
||||
"store_failures_as",
|
||||
]
|
||||
|
||||
seen = set()
|
||||
@@ -619,6 +687,8 @@ class SnapshotConfig(EmptySnapshotConfig):
|
||||
@classmethod
|
||||
def validate(cls, data):
|
||||
super().validate(data)
|
||||
# Note: currently you can't just set these keys in schema.yml because this validation
|
||||
# will fail when parsing the snapshot node.
|
||||
if not data.get("strategy") or not data.get("unique_key") or not data.get("target_schema"):
|
||||
raise ValidationError(
|
||||
"Snapshots must be configured with a 'strategy', 'unique_key', "
|
||||
@@ -649,6 +719,7 @@ class SnapshotConfig(EmptySnapshotConfig):
|
||||
if data.get("materialized") and data.get("materialized") != "snapshot":
|
||||
raise ValidationError("A snapshot must have a materialized value of 'snapshot'")
|
||||
|
||||
# Called by "calculate_node_config_dict" in ContextConfigGenerator
|
||||
def finalize_and_validate(self):
|
||||
data = self.to_dict(omit_none=True)
|
||||
self.validate(data)
|
||||
@@ -657,6 +728,8 @@ class SnapshotConfig(EmptySnapshotConfig):
|
||||
|
||||
RESOURCE_TYPES: Dict[NodeType, Type[BaseConfig]] = {
|
||||
NodeType.Metric: MetricConfig,
|
||||
NodeType.SemanticModel: SemanticModelConfig,
|
||||
NodeType.SavedQuery: SavedQueryConfig,
|
||||
NodeType.Exposure: ExposureConfig,
|
||||
NodeType.Source: SourceConfig,
|
||||
NodeType.Seed: SeedConfig,
|
||||
|
||||
@@ -29,3 +29,12 @@ class ModelNodeArgs:
|
||||
unique_id = f"{unique_id}.v{self.version}"
|
||||
|
||||
return unique_id
|
||||
|
||||
@property
|
||||
def fqn(self) -> List[str]:
|
||||
fqn = [self.package_name, self.name]
|
||||
# Test for None explicitly because version can be 0
|
||||
if self.version is not None:
|
||||
fqn.append(f"v{self.version}")
|
||||
|
||||
return fqn
|
||||
|
||||
@@ -6,12 +6,13 @@ from enum import Enum
|
||||
import hashlib
|
||||
|
||||
from mashumaro.types import SerializableType
|
||||
from typing import Optional, Union, List, Dict, Any, Sequence, Tuple, Iterator
|
||||
from typing import Optional, Union, List, Dict, Any, Sequence, Tuple, Iterator, Literal
|
||||
|
||||
from dbt.dataclass_schema import dbtClassMixin, ExtensibleDbtClassMixin
|
||||
|
||||
from dbt.clients.system import write_file
|
||||
from dbt.contracts.files import FileHash
|
||||
from dbt.contracts.graph.saved_queries import Export, QueryParams
|
||||
from dbt.contracts.graph.semantic_models import (
|
||||
Defaults,
|
||||
Dimension,
|
||||
@@ -20,6 +21,7 @@ from dbt.contracts.graph.semantic_models import (
|
||||
SourceFileMetadata,
|
||||
)
|
||||
from dbt.contracts.graph.unparsed import (
|
||||
ConstantPropertyInput,
|
||||
Docs,
|
||||
ExposureType,
|
||||
ExternalTable,
|
||||
@@ -36,6 +38,7 @@ from dbt.contracts.graph.unparsed import (
|
||||
UnparsedColumn,
|
||||
)
|
||||
from dbt.contracts.graph.node_args import ModelNodeArgs
|
||||
from dbt.contracts.graph.semantic_layer_common import WhereFilterIntersection
|
||||
from dbt.contracts.util import Replaceable, AdditionalPropertiesMixin
|
||||
from dbt.events.functions import warn_or_error
|
||||
from dbt.exceptions import ParsingError, ContractBreakingChangeError
|
||||
@@ -44,23 +47,28 @@ from dbt.events.types import (
|
||||
SeedExceedsLimitSamePath,
|
||||
SeedExceedsLimitAndPathChanged,
|
||||
SeedExceedsLimitChecksumChanged,
|
||||
UnversionedBreakingChange,
|
||||
)
|
||||
from dbt.events.contextvars import set_log_contextvars
|
||||
from dbt.flags import get_flags
|
||||
from dbt.node_types import ModelLanguage, NodeType, AccessType
|
||||
from dbt_semantic_interfaces.call_parameter_sets import FilterCallParameterSets
|
||||
from dbt_semantic_interfaces.references import (
|
||||
EntityReference,
|
||||
MeasureReference,
|
||||
LinkableElementReference,
|
||||
SemanticModelReference,
|
||||
TimeDimensionReference,
|
||||
)
|
||||
from dbt_semantic_interfaces.references import MetricReference as DSIMetricReference
|
||||
from dbt_semantic_interfaces.type_enums import MetricType, TimeGranularity
|
||||
from dbt_semantic_interfaces.parsing.where_filter_parser import WhereFilterParser
|
||||
from dbt_semantic_interfaces.type_enums import (
|
||||
ConversionCalculationType,
|
||||
MetricType,
|
||||
TimeGranularity,
|
||||
)
|
||||
|
||||
from .model_config import (
|
||||
NodeConfig,
|
||||
ModelConfig,
|
||||
SeedConfig,
|
||||
TestConfig,
|
||||
SourceConfig,
|
||||
@@ -69,6 +77,7 @@ from .model_config import (
|
||||
EmptySnapshotConfig,
|
||||
SnapshotConfig,
|
||||
SemanticModelConfig,
|
||||
SavedQueryConfig,
|
||||
)
|
||||
|
||||
|
||||
@@ -229,6 +238,7 @@ class ColumnInfo(AdditionalPropertiesMixin, ExtensibleDbtClassMixin, Replaceable
|
||||
@dataclass
|
||||
class Contract(dbtClassMixin, Replaceable):
|
||||
enforced: bool = False
|
||||
alias_types: bool = True
|
||||
checksum: Optional[str] = None
|
||||
|
||||
|
||||
@@ -554,19 +564,20 @@ class CompiledNode(ParsedNode):
|
||||
|
||||
@dataclass
|
||||
class AnalysisNode(CompiledNode):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Analysis]})
|
||||
resource_type: Literal[NodeType.Analysis]
|
||||
|
||||
|
||||
@dataclass
|
||||
class HookNode(CompiledNode):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Operation]})
|
||||
resource_type: Literal[NodeType.Operation]
|
||||
index: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelNode(CompiledNode):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Model]})
|
||||
resource_type: Literal[NodeType.Model]
|
||||
access: AccessType = AccessType.Protected
|
||||
config: ModelConfig = field(default_factory=ModelConfig)
|
||||
constraints: List[ModelLevelConstraint] = field(default_factory=list)
|
||||
version: Optional[NodeVersion] = None
|
||||
latest_version: Optional[NodeVersion] = None
|
||||
@@ -589,7 +600,7 @@ class ModelNode(CompiledNode):
|
||||
name=args.name,
|
||||
package_name=args.package_name,
|
||||
unique_id=unique_id,
|
||||
fqn=[args.package_name, args.name],
|
||||
fqn=args.fqn,
|
||||
version=args.version,
|
||||
latest_version=args.latest_version,
|
||||
relation_name=args.relation_name,
|
||||
@@ -603,7 +614,7 @@ class ModelNode(CompiledNode):
|
||||
path="",
|
||||
unrendered_config=unrendered_config,
|
||||
depends_on=DependsOn(nodes=args.depends_on_nodes),
|
||||
config=NodeConfig(enabled=args.enabled),
|
||||
config=ModelConfig(enabled=args.enabled),
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -625,6 +636,18 @@ class ModelNode(CompiledNode):
|
||||
def materialization_enforces_constraints(self) -> bool:
|
||||
return self.config.materialized in ["table", "incremental"]
|
||||
|
||||
def same_contents(self, old, adapter_type) -> bool:
|
||||
return super().same_contents(old, adapter_type) and self.same_ref_representation(old)
|
||||
|
||||
def same_ref_representation(self, old) -> bool:
|
||||
return (
|
||||
# Changing the latest_version may break downstream unpinned refs
|
||||
self.latest_version == old.latest_version
|
||||
# Changes to access or deprecation_date may lead to ref-related parsing errors
|
||||
and self.access == old.access
|
||||
and self.deprecation_date == old.deprecation_date
|
||||
)
|
||||
|
||||
def build_contract_checksum(self):
|
||||
# We don't need to construct the checksum if the model does not
|
||||
# have contract enforced, because it won't be used.
|
||||
@@ -669,11 +692,11 @@ class ModelNode(CompiledNode):
|
||||
# These are the categories of breaking changes:
|
||||
contract_enforced_disabled: bool = False
|
||||
columns_removed: List[str] = []
|
||||
column_type_changes: List[Tuple[str, str, str]] = []
|
||||
enforced_column_constraint_removed: List[Tuple[str, str]] = [] # column, constraint_type
|
||||
enforced_model_constraint_removed: List[
|
||||
Tuple[str, List[str]]
|
||||
] = [] # constraint_type, columns
|
||||
column_type_changes: List[Dict[str, str]] = []
|
||||
enforced_column_constraint_removed: List[
|
||||
Dict[str, str]
|
||||
] = [] # column_name, constraint_type
|
||||
enforced_model_constraint_removed: List[Dict[str, Any]] = [] # constraint_type, columns
|
||||
materialization_changed: List[str] = []
|
||||
|
||||
if old.contract.enforced is True and self.contract.enforced is False:
|
||||
@@ -695,11 +718,11 @@ class ModelNode(CompiledNode):
|
||||
# Has this column's data type changed?
|
||||
elif old_value.data_type != self.columns[old_key].data_type:
|
||||
column_type_changes.append(
|
||||
(
|
||||
str(old_value.name),
|
||||
str(old_value.data_type),
|
||||
str(self.columns[old_key].data_type),
|
||||
)
|
||||
{
|
||||
"column_name": str(old_value.name),
|
||||
"previous_column_type": str(old_value.data_type),
|
||||
"current_column_type": str(self.columns[old_key].data_type),
|
||||
}
|
||||
)
|
||||
|
||||
# track if there are any column level constraints for the materialization check late
|
||||
@@ -720,7 +743,11 @@ class ModelNode(CompiledNode):
|
||||
and constraint_support[old_constraint.type] == ConstraintSupport.ENFORCED
|
||||
):
|
||||
enforced_column_constraint_removed.append(
|
||||
(old_key, str(old_constraint.type))
|
||||
{
|
||||
"column_name": old_key,
|
||||
"constraint_name": old_constraint.name,
|
||||
"constraint_type": ConstraintType(old_constraint.type),
|
||||
}
|
||||
)
|
||||
|
||||
# Now compare the model level constraints
|
||||
@@ -731,7 +758,11 @@ class ModelNode(CompiledNode):
|
||||
and constraint_support[old_constraint.type] == ConstraintSupport.ENFORCED
|
||||
):
|
||||
enforced_model_constraint_removed.append(
|
||||
(str(old_constraint.type), old_constraint.columns)
|
||||
{
|
||||
"constraint_name": old_constraint.name,
|
||||
"constraint_type": ConstraintType(old_constraint.type),
|
||||
"columns": old_constraint.columns,
|
||||
}
|
||||
)
|
||||
|
||||
# Check for relevant materialization changes.
|
||||
@@ -745,7 +776,8 @@ class ModelNode(CompiledNode):
|
||||
# If a column has been added, it will be missing in the old.columns, and present in self.columns
|
||||
# That's a change (caught by the different checksums), but not a breaking change
|
||||
|
||||
# Did we find any changes that we consider breaking? If so, that's an error
|
||||
# Did we find any changes that we consider breaking? If there's an enforced contract, that's
|
||||
# a warning unless the model is versioned, then it's an error.
|
||||
if (
|
||||
contract_enforced_disabled
|
||||
or columns_removed
|
||||
@@ -754,32 +786,89 @@ class ModelNode(CompiledNode):
|
||||
or enforced_column_constraint_removed
|
||||
or materialization_changed
|
||||
):
|
||||
raise (
|
||||
ContractBreakingChangeError(
|
||||
contract_enforced_disabled=contract_enforced_disabled,
|
||||
columns_removed=columns_removed,
|
||||
column_type_changes=column_type_changes,
|
||||
enforced_column_constraint_removed=enforced_column_constraint_removed,
|
||||
enforced_model_constraint_removed=enforced_model_constraint_removed,
|
||||
materialization_changed=materialization_changed,
|
||||
|
||||
breaking_changes = []
|
||||
if contract_enforced_disabled:
|
||||
breaking_changes.append(
|
||||
"Contract enforcement was removed: Previously, this model had an enforced contract. It is no longer configured to enforce its contract, and this is a breaking change."
|
||||
)
|
||||
if columns_removed:
|
||||
columns_removed_str = "\n - ".join(columns_removed)
|
||||
breaking_changes.append(f"Columns were removed: \n - {columns_removed_str}")
|
||||
if column_type_changes:
|
||||
column_type_changes_str = "\n - ".join(
|
||||
[
|
||||
f"{c['column_name']} ({c['previous_column_type']} -> {c['current_column_type']})"
|
||||
for c in column_type_changes
|
||||
]
|
||||
)
|
||||
breaking_changes.append(
|
||||
f"Columns with data_type changes: \n - {column_type_changes_str}"
|
||||
)
|
||||
if enforced_column_constraint_removed:
|
||||
column_constraint_changes_str = "\n - ".join(
|
||||
[
|
||||
f"'{c['constraint_name'] if c['constraint_name'] is not None else c['constraint_type']}' constraint on column {c['column_name']}"
|
||||
for c in enforced_column_constraint_removed
|
||||
]
|
||||
)
|
||||
breaking_changes.append(
|
||||
f"Enforced column level constraints were removed: \n - {column_constraint_changes_str}"
|
||||
)
|
||||
if enforced_model_constraint_removed:
|
||||
model_constraint_changes_str = "\n - ".join(
|
||||
[
|
||||
f"'{c['constraint_name'] if c['constraint_name'] is not None else c['constraint_type']}' constraint on columns {c['columns']}"
|
||||
for c in enforced_model_constraint_removed
|
||||
]
|
||||
)
|
||||
breaking_changes.append(
|
||||
f"Enforced model level constraints were removed: \n - {model_constraint_changes_str}"
|
||||
)
|
||||
if materialization_changed:
|
||||
materialization_changes_str = (
|
||||
f"{materialization_changed[0]} -> {materialization_changed[1]}"
|
||||
)
|
||||
|
||||
breaking_changes.append(
|
||||
f"Materialization changed with enforced constraints: \n - {materialization_changes_str}"
|
||||
)
|
||||
|
||||
if self.version is None:
|
||||
warn_or_error(
|
||||
UnversionedBreakingChange(
|
||||
contract_enforced_disabled=contract_enforced_disabled,
|
||||
columns_removed=columns_removed,
|
||||
column_type_changes=column_type_changes,
|
||||
enforced_column_constraint_removed=enforced_column_constraint_removed,
|
||||
enforced_model_constraint_removed=enforced_model_constraint_removed,
|
||||
breaking_changes=breaking_changes,
|
||||
model_name=self.name,
|
||||
model_file_path=self.original_file_path,
|
||||
),
|
||||
node=self,
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise (
|
||||
ContractBreakingChangeError(
|
||||
breaking_changes=breaking_changes,
|
||||
node=self,
|
||||
)
|
||||
)
|
||||
|
||||
# Otherwise, though we didn't find any *breaking* changes, the contract has still changed -- same_contract: False
|
||||
else:
|
||||
return False
|
||||
# Otherwise, the contract has changed -- same_contract: False
|
||||
return False
|
||||
|
||||
|
||||
# TODO: rm?
|
||||
@dataclass
|
||||
class RPCNode(CompiledNode):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.RPCCall]})
|
||||
resource_type: Literal[NodeType.RPCCall]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SqlNode(CompiledNode):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.SqlOperation]})
|
||||
resource_type: Literal[NodeType.SqlOperation]
|
||||
|
||||
|
||||
# ====================================
|
||||
@@ -789,7 +878,7 @@ class SqlNode(CompiledNode):
|
||||
|
||||
@dataclass
|
||||
class SeedNode(ParsedNode): # No SQLDefaults!
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Seed]})
|
||||
resource_type: Literal[NodeType.Seed]
|
||||
config: SeedConfig = field(default_factory=SeedConfig)
|
||||
# seeds need the root_path because the contents are not loaded initially
|
||||
# and we need the root_path to load the seed later
|
||||
@@ -915,7 +1004,7 @@ class TestShouldStoreFailures:
|
||||
|
||||
@dataclass
|
||||
class SingularTestNode(TestShouldStoreFailures, CompiledNode):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Test]})
|
||||
resource_type: Literal[NodeType.Test]
|
||||
# Was not able to make mypy happy and keep the code working. We need to
|
||||
# refactor the various configs.
|
||||
config: TestConfig = field(default_factory=TestConfig) # type: ignore
|
||||
@@ -951,7 +1040,7 @@ class HasTestMetadata(dbtClassMixin):
|
||||
|
||||
@dataclass
|
||||
class GenericTestNode(TestShouldStoreFailures, CompiledNode, HasTestMetadata):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Test]})
|
||||
resource_type: Literal[NodeType.Test]
|
||||
column_name: Optional[str] = None
|
||||
file_key_name: Optional[str] = None
|
||||
# Was not able to make mypy happy and keep the code working. We need to
|
||||
@@ -984,13 +1073,13 @@ class IntermediateSnapshotNode(CompiledNode):
|
||||
# uses a regular node config, which the snapshot parser will then convert
|
||||
# into a full ParsedSnapshotNode after rendering. Note: it currently does
|
||||
# not work to set snapshot config in schema files because of the validation.
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Snapshot]})
|
||||
resource_type: Literal[NodeType.Snapshot]
|
||||
config: EmptySnapshotConfig = field(default_factory=EmptySnapshotConfig)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SnapshotNode(CompiledNode):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Snapshot]})
|
||||
resource_type: Literal[NodeType.Snapshot]
|
||||
config: SnapshotConfig
|
||||
defer_relation: Optional[DeferRelation] = None
|
||||
|
||||
@@ -1003,7 +1092,7 @@ class SnapshotNode(CompiledNode):
|
||||
@dataclass
|
||||
class Macro(BaseNode):
|
||||
macro_sql: str
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Macro]})
|
||||
resource_type: Literal[NodeType.Macro]
|
||||
depends_on: MacroDependsOn = field(default_factory=MacroDependsOn)
|
||||
description: str = ""
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
@@ -1033,7 +1122,7 @@ class Macro(BaseNode):
|
||||
@dataclass
|
||||
class Documentation(BaseNode):
|
||||
block_contents: str
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Documentation]})
|
||||
resource_type: Literal[NodeType.Documentation]
|
||||
|
||||
@property
|
||||
def search_name(self):
|
||||
@@ -1064,7 +1153,7 @@ class UnpatchedSourceDefinition(BaseNode):
|
||||
source: UnparsedSourceDefinition
|
||||
table: UnparsedSourceTableDefinition
|
||||
fqn: List[str]
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Source]})
|
||||
resource_type: Literal[NodeType.Source]
|
||||
patch_path: Optional[str] = None
|
||||
|
||||
def get_full_source_name(self):
|
||||
@@ -1109,7 +1198,7 @@ class ParsedSourceMandatory(GraphNode, HasRelationMetadata):
|
||||
source_description: str
|
||||
loader: str
|
||||
identifier: str
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Source]})
|
||||
resource_type: Literal[NodeType.Source]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -1219,8 +1308,8 @@ class SourceDefinition(NodeInfoMixin, ParsedSourceMandatory):
|
||||
return []
|
||||
|
||||
@property
|
||||
def has_freshness(self):
|
||||
return bool(self.freshness) and self.loaded_at_field is not None
|
||||
def has_freshness(self) -> bool:
|
||||
return bool(self.freshness)
|
||||
|
||||
@property
|
||||
def search_name(self):
|
||||
@@ -1236,7 +1325,7 @@ class SourceDefinition(NodeInfoMixin, ParsedSourceMandatory):
|
||||
class Exposure(GraphNode):
|
||||
type: ExposureType
|
||||
owner: Owner
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Exposure]})
|
||||
resource_type: Literal[NodeType.Exposure]
|
||||
description: str = ""
|
||||
label: Optional[str] = None
|
||||
maturity: Optional[MaturityType] = None
|
||||
@@ -1315,20 +1404,13 @@ class Exposure(GraphNode):
|
||||
# ====================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class WhereFilter(dbtClassMixin):
|
||||
where_sql_template: str
|
||||
|
||||
@property
|
||||
def call_parameter_sets(self) -> FilterCallParameterSets:
|
||||
return WhereFilterParser.parse_call_parameter_sets(self.where_sql_template)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetricInputMeasure(dbtClassMixin):
|
||||
name: str
|
||||
filter: Optional[WhereFilter] = None
|
||||
filter: Optional[WhereFilterIntersection] = None
|
||||
alias: Optional[str] = None
|
||||
join_to_timespine: bool = False
|
||||
fill_nulls_with: Optional[int] = None
|
||||
|
||||
def measure_reference(self) -> MeasureReference:
|
||||
return MeasureReference(element_name=self.name)
|
||||
@@ -1346,7 +1428,7 @@ class MetricTimeWindow(dbtClassMixin):
|
||||
@dataclass
|
||||
class MetricInput(dbtClassMixin):
|
||||
name: str
|
||||
filter: Optional[WhereFilter] = None
|
||||
filter: Optional[WhereFilterIntersection] = None
|
||||
alias: Optional[str] = None
|
||||
offset_window: Optional[MetricTimeWindow] = None
|
||||
offset_to_grain: Optional[TimeGranularity] = None
|
||||
@@ -1358,6 +1440,16 @@ class MetricInput(dbtClassMixin):
|
||||
return DSIMetricReference(element_name=self.alias or self.name)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConversionTypeParams(dbtClassMixin):
|
||||
base_measure: MetricInputMeasure
|
||||
conversion_measure: MetricInputMeasure
|
||||
entity: str
|
||||
calculation: ConversionCalculationType = ConversionCalculationType.CONVERSION_RATE
|
||||
window: Optional[MetricTimeWindow] = None
|
||||
constant_properties: Optional[List[ConstantPropertyInput]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetricTypeParams(dbtClassMixin):
|
||||
measure: Optional[MetricInputMeasure] = None
|
||||
@@ -1368,6 +1460,7 @@ class MetricTypeParams(dbtClassMixin):
|
||||
window: Optional[MetricTimeWindow] = None
|
||||
grain_to_date: Optional[TimeGranularity] = None
|
||||
metrics: Optional[List[MetricInput]] = None
|
||||
conversion_type_params: Optional[ConversionTypeParams] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -1383,9 +1476,9 @@ class Metric(GraphNode):
|
||||
label: str
|
||||
type: MetricType
|
||||
type_params: MetricTypeParams
|
||||
filter: Optional[WhereFilter] = None
|
||||
filter: Optional[WhereFilterIntersection] = None
|
||||
metadata: Optional[SourceFileMetadata] = None
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Metric]})
|
||||
resource_type: Literal[NodeType.Metric]
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
tags: List[str] = field(default_factory=list)
|
||||
config: MetricConfig = field(default_factory=MetricConfig)
|
||||
@@ -1468,7 +1561,7 @@ class Metric(GraphNode):
|
||||
class Group(BaseNode):
|
||||
name: str
|
||||
owner: Owner
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Group]})
|
||||
resource_type: Literal[NodeType.Group]
|
||||
|
||||
|
||||
# ====================================
|
||||
@@ -1489,6 +1582,7 @@ class SemanticModel(GraphNode):
|
||||
model: str
|
||||
node_relation: Optional[NodeRelation]
|
||||
description: Optional[str] = None
|
||||
label: Optional[str] = None
|
||||
defaults: Optional[Defaults] = None
|
||||
entities: Sequence[Entity] = field(default_factory=list)
|
||||
measures: Sequence[Measure] = field(default_factory=list)
|
||||
@@ -1498,6 +1592,9 @@ class SemanticModel(GraphNode):
|
||||
refs: List[RefArgs] = field(default_factory=list)
|
||||
created_at: float = field(default_factory=lambda: time.time())
|
||||
config: SemanticModelConfig = field(default_factory=SemanticModelConfig)
|
||||
unrendered_config: Dict[str, Any] = field(default_factory=dict)
|
||||
primary_entity: Optional[str] = None
|
||||
group: Optional[str] = None
|
||||
|
||||
@property
|
||||
def entity_references(self) -> List[LinkableElementReference]:
|
||||
@@ -1568,17 +1665,157 @@ class SemanticModel(GraphNode):
|
||||
measure is not None
|
||||
), f"No measure with name ({measure_reference.element_name}) in semantic_model with name ({self.name})"
|
||||
|
||||
if self.defaults is not None:
|
||||
default_agg_time_dimesion = self.defaults.agg_time_dimension
|
||||
default_agg_time_dimension = (
|
||||
self.defaults.agg_time_dimension if self.defaults is not None else None
|
||||
)
|
||||
|
||||
agg_time_dimension_name = measure.agg_time_dimension or default_agg_time_dimesion
|
||||
agg_time_dimension_name = measure.agg_time_dimension or default_agg_time_dimension
|
||||
assert agg_time_dimension_name is not None, (
|
||||
f"Aggregation time dimension for measure {measure.name} is not set! This should either be set directly on "
|
||||
f"the measure specification in the model, or else defaulted to the primary time dimension in the data "
|
||||
f"source containing the measure."
|
||||
f"Aggregation time dimension for measure {measure.name} on semantic model {self.name} is not set! "
|
||||
"To fix this either specify a default `agg_time_dimension` for the semantic model or define an "
|
||||
"`agg_time_dimension` on the measure directly."
|
||||
)
|
||||
return TimeDimensionReference(element_name=agg_time_dimension_name)
|
||||
|
||||
@property
|
||||
def primary_entity_reference(self) -> Optional[EntityReference]:
|
||||
return (
|
||||
EntityReference(element_name=self.primary_entity)
|
||||
if self.primary_entity is not None
|
||||
else None
|
||||
)
|
||||
|
||||
def same_model(self, old: "SemanticModel") -> bool:
|
||||
return self.model == old.same_model
|
||||
|
||||
def same_node_relation(self, old: "SemanticModel") -> bool:
|
||||
return self.node_relation == old.node_relation
|
||||
|
||||
def same_description(self, old: "SemanticModel") -> bool:
|
||||
return self.description == old.description
|
||||
|
||||
def same_defaults(self, old: "SemanticModel") -> bool:
|
||||
return self.defaults == old.defaults
|
||||
|
||||
def same_entities(self, old: "SemanticModel") -> bool:
|
||||
return self.entities == old.entities
|
||||
|
||||
def same_dimensions(self, old: "SemanticModel") -> bool:
|
||||
return self.dimensions == old.dimensions
|
||||
|
||||
def same_measures(self, old: "SemanticModel") -> bool:
|
||||
return self.measures == old.measures
|
||||
|
||||
def same_config(self, old: "SemanticModel") -> bool:
|
||||
return self.config == old.config
|
||||
|
||||
def same_primary_entity(self, old: "SemanticModel") -> bool:
|
||||
return self.primary_entity == old.primary_entity
|
||||
|
||||
def same_group(self, old: "SemanticModel") -> bool:
|
||||
return self.group == old.group
|
||||
|
||||
def same_contents(self, old: Optional["SemanticModel"]) -> bool:
|
||||
# existing when it didn't before is a change!
|
||||
# metadata/tags changes are not "changes"
|
||||
if old is None:
|
||||
return True
|
||||
|
||||
return (
|
||||
self.same_model(old)
|
||||
and self.same_node_relation(old)
|
||||
and self.same_description(old)
|
||||
and self.same_defaults(old)
|
||||
and self.same_entities(old)
|
||||
and self.same_dimensions(old)
|
||||
and self.same_measures(old)
|
||||
and self.same_config(old)
|
||||
and self.same_primary_entity(old)
|
||||
and self.same_group(old)
|
||||
and True
|
||||
)
|
||||
|
||||
|
||||
# ====================================
|
||||
# SavedQuery and related classes
|
||||
# ====================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class SavedQueryMandatory(GraphNode):
|
||||
query_params: QueryParams
|
||||
exports: List[Export]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SavedQuery(NodeInfoMixin, SavedQueryMandatory):
|
||||
description: Optional[str] = None
|
||||
label: Optional[str] = None
|
||||
metadata: Optional[SourceFileMetadata] = None
|
||||
config: SavedQueryConfig = field(default_factory=SavedQueryConfig)
|
||||
unrendered_config: Dict[str, Any] = field(default_factory=dict)
|
||||
group: Optional[str] = None
|
||||
depends_on: DependsOn = field(default_factory=DependsOn)
|
||||
created_at: float = field(default_factory=lambda: time.time())
|
||||
refs: List[RefArgs] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def metrics(self) -> List[str]:
|
||||
return self.query_params.metrics
|
||||
|
||||
@property
|
||||
def depends_on_nodes(self):
|
||||
return self.depends_on.nodes
|
||||
|
||||
def same_metrics(self, old: "SavedQuery") -> bool:
|
||||
return self.query_params.metrics == old.query_params.metrics
|
||||
|
||||
def same_group_by(self, old: "SavedQuery") -> bool:
|
||||
return self.query_params.group_by == old.query_params.group_by
|
||||
|
||||
def same_description(self, old: "SavedQuery") -> bool:
|
||||
return self.description == old.description
|
||||
|
||||
def same_where(self, old: "SavedQuery") -> bool:
|
||||
return self.query_params.where == old.query_params.where
|
||||
|
||||
def same_label(self, old: "SavedQuery") -> bool:
|
||||
return self.label == old.label
|
||||
|
||||
def same_config(self, old: "SavedQuery") -> bool:
|
||||
return self.config == old.config
|
||||
|
||||
def same_group(self, old: "SavedQuery") -> bool:
|
||||
return self.group == old.group
|
||||
|
||||
def same_exports(self, old: "SavedQuery") -> bool:
|
||||
if len(self.exports) != len(old.exports):
|
||||
return False
|
||||
|
||||
# exports should be in the same order, so we zip them for easy iteration
|
||||
for (old_export, new_export) in zip(old.exports, self.exports):
|
||||
if not new_export.same_contents(old_export):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def same_contents(self, old: Optional["SavedQuery"]) -> bool:
|
||||
# existing when it didn't before is a change!
|
||||
# metadata/tags changes are not "changes"
|
||||
if old is None:
|
||||
return True
|
||||
|
||||
return (
|
||||
self.same_metrics(old)
|
||||
and self.same_group_by(old)
|
||||
and self.same_description(old)
|
||||
and self.same_where(old)
|
||||
and self.same_label(old)
|
||||
and self.same_config(old)
|
||||
and self.same_group(old)
|
||||
and True
|
||||
)
|
||||
|
||||
|
||||
# ====================================
|
||||
# Patches
|
||||
@@ -1646,6 +1883,7 @@ GraphMemberNode = Union[
|
||||
ResultNode,
|
||||
Exposure,
|
||||
Metric,
|
||||
SavedQuery,
|
||||
SemanticModel,
|
||||
]
|
||||
|
||||
|
||||
53
core/dbt/contracts/graph/saved_queries.py
Normal file
53
core/dbt/contracts/graph/saved_queries.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dbt.contracts.graph.semantic_layer_common import WhereFilterIntersection
|
||||
from dbt.dataclass_schema import dbtClassMixin
|
||||
from dbt_semantic_interfaces.type_enums.export_destination_type import ExportDestinationType
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExportConfig(dbtClassMixin):
|
||||
"""Nested configuration attributes for exports."""
|
||||
|
||||
export_as: ExportDestinationType
|
||||
schema_name: Optional[str] = None
|
||||
alias: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Export(dbtClassMixin):
|
||||
"""Configuration for writing query results to a table."""
|
||||
|
||||
name: str
|
||||
config: ExportConfig
|
||||
|
||||
def same_name(self, old: Export) -> bool:
|
||||
return self.name == old.name
|
||||
|
||||
def same_export_as(self, old: Export) -> bool:
|
||||
return self.config.export_as == old.config.export_as
|
||||
|
||||
def same_schema_name(self, old: Export) -> bool:
|
||||
return self.config.schema_name == old.config.schema_name
|
||||
|
||||
def same_alias(self, old: Export) -> bool:
|
||||
return self.config.alias == old.config.alias
|
||||
|
||||
def same_contents(self, old: Export) -> bool:
|
||||
return (
|
||||
self.same_name(old)
|
||||
and self.same_export_as(old)
|
||||
and self.same_schema_name(old)
|
||||
and self.same_alias(old)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueryParams(dbtClassMixin):
|
||||
"""The query parameters for the saved query"""
|
||||
|
||||
metrics: List[str]
|
||||
group_by: List[str]
|
||||
where: Optional[WhereFilterIntersection]
|
||||
23
core/dbt/contracts/graph/semantic_layer_common.py
Normal file
23
core/dbt/contracts/graph/semantic_layer_common.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from dataclasses import dataclass
|
||||
from dbt.dataclass_schema import dbtClassMixin
|
||||
from dbt_semantic_interfaces.call_parameter_sets import FilterCallParameterSets
|
||||
from dbt_semantic_interfaces.parsing.where_filter.where_filter_parser import WhereFilterParser
|
||||
from typing import List, Sequence, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class WhereFilter(dbtClassMixin):
|
||||
where_sql_template: str
|
||||
|
||||
@property
|
||||
def call_parameter_sets(self) -> FilterCallParameterSets:
|
||||
return WhereFilterParser.parse_call_parameter_sets(self.where_sql_template)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WhereFilterIntersection(dbtClassMixin):
|
||||
where_filters: List[WhereFilter]
|
||||
|
||||
@property
|
||||
def filter_expression_parameter_sets(self) -> Sequence[Tuple[str, FilterCallParameterSets]]:
|
||||
raise NotImplementedError
|
||||
@@ -2,6 +2,7 @@ from dbt_semantic_interfaces.implementations.metric import PydanticMetric
|
||||
from dbt_semantic_interfaces.implementations.project_configuration import (
|
||||
PydanticProjectConfiguration,
|
||||
)
|
||||
from dbt_semantic_interfaces.implementations.saved_query import PydanticSavedQuery
|
||||
from dbt_semantic_interfaces.implementations.semantic_manifest import PydanticSemanticManifest
|
||||
from dbt_semantic_interfaces.implementations.semantic_model import PydanticSemanticModel
|
||||
from dbt_semantic_interfaces.implementations.time_spine_table_configuration import (
|
||||
@@ -20,7 +21,7 @@ from dbt.exceptions import ParsingError
|
||||
|
||||
|
||||
class SemanticManifest:
|
||||
def __init__(self, manifest):
|
||||
def __init__(self, manifest) -> None:
|
||||
self.manifest = manifest
|
||||
|
||||
def validate(self) -> bool:
|
||||
@@ -71,6 +72,11 @@ class SemanticManifest:
|
||||
for metric in self.manifest.metrics.values():
|
||||
pydantic_semantic_manifest.metrics.append(PydanticMetric.parse_obj(metric.to_dict()))
|
||||
|
||||
for saved_query in self.manifest.saved_queries.values():
|
||||
pydantic_semantic_manifest.saved_queries.append(
|
||||
PydanticSavedQuery.parse_obj(saved_query.to_dict())
|
||||
)
|
||||
|
||||
# Look for time-spine table model and create time spine table configuration
|
||||
if self.manifest.semantic_models:
|
||||
# Get model for time_spine_table
|
||||
|
||||
@@ -66,6 +66,7 @@ class Dimension(dbtClassMixin):
|
||||
name: str
|
||||
type: DimensionType
|
||||
description: Optional[str] = None
|
||||
label: Optional[str] = None
|
||||
is_partition: bool = False
|
||||
type_params: Optional[DimensionTypeParams] = None
|
||||
expr: Optional[str] = None
|
||||
@@ -100,6 +101,7 @@ class Entity(dbtClassMixin):
|
||||
name: str
|
||||
type: EntityType
|
||||
description: Optional[str] = None
|
||||
label: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
expr: Optional[str] = None
|
||||
|
||||
@@ -136,6 +138,7 @@ class Measure(dbtClassMixin):
|
||||
name: str
|
||||
agg: AggregationType
|
||||
description: Optional[str] = None
|
||||
label: Optional[str] = None
|
||||
create_metric: bool = False
|
||||
expr: Optional[str] = None
|
||||
agg_params: Optional[MeasureAggregationParameters] = None
|
||||
|
||||
@@ -19,11 +19,12 @@ import dbt.helper_types # noqa:F401
|
||||
from dbt.exceptions import CompilationError, ParsingError, DbtInternalError
|
||||
|
||||
from dbt.dataclass_schema import dbtClassMixin, StrEnum, ExtensibleDbtClassMixin, ValidationError
|
||||
from dbt_semantic_interfaces.type_enums import ConversionCalculationType
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Union, Dict, Any, Sequence
|
||||
from typing import Optional, List, Union, Dict, Any, Sequence, Literal
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -49,31 +50,18 @@ class HasCode(dbtClassMixin):
|
||||
|
||||
@dataclass
|
||||
class UnparsedMacro(UnparsedBaseNode, HasCode):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Macro]})
|
||||
resource_type: Literal[NodeType.Macro]
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnparsedGenericTest(UnparsedBaseNode, HasCode):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Macro]})
|
||||
resource_type: Literal[NodeType.Macro]
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnparsedNode(UnparsedBaseNode, HasCode):
|
||||
name: str
|
||||
resource_type: NodeType = field(
|
||||
metadata={
|
||||
"restrict": [
|
||||
NodeType.Model,
|
||||
NodeType.Analysis,
|
||||
NodeType.Test,
|
||||
NodeType.Snapshot,
|
||||
NodeType.Operation,
|
||||
NodeType.Seed,
|
||||
NodeType.RPCCall,
|
||||
NodeType.SqlOperation,
|
||||
]
|
||||
}
|
||||
)
|
||||
resource_type: NodeType
|
||||
|
||||
@property
|
||||
def search_name(self):
|
||||
@@ -82,7 +70,7 @@ class UnparsedNode(UnparsedBaseNode, HasCode):
|
||||
|
||||
@dataclass
|
||||
class UnparsedRunHook(UnparsedNode):
|
||||
resource_type: NodeType = field(metadata={"restrict": [NodeType.Operation]})
|
||||
resource_type: Literal[NodeType.Operation]
|
||||
index: Optional[int] = None
|
||||
|
||||
|
||||
@@ -163,14 +151,9 @@ class UnparsedVersion(dbtClassMixin):
|
||||
|
||||
def __lt__(self, other):
|
||||
try:
|
||||
v = type(other.v)(self.v)
|
||||
return v < other.v
|
||||
return float(self.v) < float(other.v)
|
||||
except ValueError:
|
||||
try:
|
||||
other_v = type(self.v)(other.v)
|
||||
return self.v < other_v
|
||||
except ValueError:
|
||||
return str(self.v) < str(other.v)
|
||||
return str(self.v) < str(other.v)
|
||||
|
||||
@property
|
||||
def include_exclude(self) -> dbt.helper_types.IncludeExclude:
|
||||
@@ -220,7 +203,7 @@ class UnparsedModelUpdate(UnparsedNodeUpdate):
|
||||
versions: Sequence[UnparsedVersion] = field(default_factory=list)
|
||||
deprecation_date: Optional[datetime.datetime] = None
|
||||
|
||||
def __post_init__(self):
|
||||
def __post_init__(self) -> None:
|
||||
if self.latest_version:
|
||||
version_values = [version.v for version in self.versions]
|
||||
if self.latest_version not in version_values:
|
||||
@@ -228,7 +211,7 @@ class UnparsedModelUpdate(UnparsedNodeUpdate):
|
||||
f"latest_version: {self.latest_version} is not one of model '{self.name}' versions: {version_values} "
|
||||
)
|
||||
|
||||
seen_versions: set[str] = set()
|
||||
seen_versions = set()
|
||||
for version in self.versions:
|
||||
if str(version.v) in seen_versions:
|
||||
raise ParsingError(
|
||||
@@ -600,19 +583,39 @@ class MetricTime(dbtClassMixin, Mergeable):
|
||||
@dataclass
|
||||
class UnparsedMetricInputMeasure(dbtClassMixin):
|
||||
name: str
|
||||
filter: Optional[str] = None
|
||||
filter: Optional[Union[str, List[str]]] = None
|
||||
alias: Optional[str] = None
|
||||
join_to_timespine: bool = False
|
||||
fill_nulls_with: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnparsedMetricInput(dbtClassMixin):
|
||||
name: str
|
||||
filter: Optional[str] = None
|
||||
filter: Optional[Union[str, List[str]]] = None
|
||||
alias: Optional[str] = None
|
||||
offset_window: Optional[str] = None
|
||||
offset_to_grain: Optional[str] = None # str is really a TimeGranularity Enum
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConstantPropertyInput(dbtClassMixin):
|
||||
base_property: str
|
||||
conversion_property: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnparsedConversionTypeParams(dbtClassMixin):
|
||||
base_measure: Union[UnparsedMetricInputMeasure, str]
|
||||
conversion_measure: Union[UnparsedMetricInputMeasure, str]
|
||||
entity: str
|
||||
calculation: str = (
|
||||
ConversionCalculationType.CONVERSION_RATE.value
|
||||
) # ConversionCalculationType Enum
|
||||
window: Optional[str] = None
|
||||
constant_properties: Optional[List[ConstantPropertyInput]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnparsedMetricTypeParams(dbtClassMixin):
|
||||
measure: Optional[Union[UnparsedMetricInputMeasure, str]] = None
|
||||
@@ -622,6 +625,7 @@ class UnparsedMetricTypeParams(dbtClassMixin):
|
||||
window: Optional[str] = None
|
||||
grain_to_date: Optional[str] = None # str is really a TimeGranularity Enum
|
||||
metrics: Optional[List[Union[UnparsedMetricInput, str]]] = None
|
||||
conversion_type_params: Optional[UnparsedConversionTypeParams] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -631,7 +635,7 @@ class UnparsedMetric(dbtClassMixin):
|
||||
type: str
|
||||
type_params: UnparsedMetricTypeParams
|
||||
description: str = ""
|
||||
filter: Optional[str] = None
|
||||
filter: Optional[Union[str, List[str]]] = None
|
||||
# metadata: Optional[Unparsedetadata] = None # TODO
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
tags: List[str] = field(default_factory=list)
|
||||
@@ -681,6 +685,7 @@ class UnparsedEntity(dbtClassMixin):
|
||||
name: str
|
||||
type: str # EntityType enum
|
||||
description: Optional[str] = None
|
||||
label: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
expr: Optional[str] = None
|
||||
|
||||
@@ -689,7 +694,7 @@ class UnparsedEntity(dbtClassMixin):
|
||||
class UnparsedNonAdditiveDimension(dbtClassMixin):
|
||||
name: str
|
||||
window_choice: str # AggregationType enum
|
||||
window_groupings: List[str]
|
||||
window_groupings: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -697,10 +702,12 @@ class UnparsedMeasure(dbtClassMixin):
|
||||
name: str
|
||||
agg: str # actually an enum
|
||||
description: Optional[str] = None
|
||||
label: Optional[str] = None
|
||||
expr: Optional[Union[str, bool, int]] = None
|
||||
agg_params: Optional[MeasureAggregationParameters] = None
|
||||
non_additive_dimension: Optional[UnparsedNonAdditiveDimension] = None
|
||||
agg_time_dimension: Optional[str] = None
|
||||
create_metric: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -714,6 +721,7 @@ class UnparsedDimension(dbtClassMixin):
|
||||
name: str
|
||||
type: str # actually an enum
|
||||
description: Optional[str] = None
|
||||
label: Optional[str] = None
|
||||
is_partition: bool = False
|
||||
type_params: Optional[UnparsedDimensionTypeParams] = None
|
||||
expr: Optional[str] = None
|
||||
@@ -723,11 +731,39 @@ class UnparsedDimension(dbtClassMixin):
|
||||
class UnparsedSemanticModel(dbtClassMixin):
|
||||
name: str
|
||||
model: str # looks like "ref(...)"
|
||||
config: Dict[str, Any] = field(default_factory=dict)
|
||||
description: Optional[str] = None
|
||||
label: Optional[str] = None
|
||||
defaults: Optional[Defaults] = None
|
||||
entities: List[UnparsedEntity] = field(default_factory=list)
|
||||
measures: List[UnparsedMeasure] = field(default_factory=list)
|
||||
dimensions: List[UnparsedDimension] = field(default_factory=list)
|
||||
primary_entity: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnparsedQueryParams(dbtClassMixin):
|
||||
metrics: List[str] = field(default_factory=list)
|
||||
group_by: List[str] = field(default_factory=list)
|
||||
where: Optional[Union[str, List[str]]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnparsedExport(dbtClassMixin):
|
||||
"""Configuration for writing query results to a table."""
|
||||
|
||||
name: str
|
||||
config: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnparsedSavedQuery(dbtClassMixin):
|
||||
name: str
|
||||
query_params: UnparsedQueryParams
|
||||
description: Optional[str] = None
|
||||
label: Optional[str] = None
|
||||
exports: List[UnparsedExport] = field(default_factory=list)
|
||||
config: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
def normalize_date(d: Optional[datetime.date]) -> Optional[datetime.datetime]:
|
||||
|
||||
@@ -4,13 +4,14 @@ from dbt.helper_types import NoValue
|
||||
from dbt.dataclass_schema import (
|
||||
dbtClassMixin,
|
||||
ValidationError,
|
||||
HyphenatedDbtClassMixin,
|
||||
ExtensibleDbtClassMixin,
|
||||
register_pattern,
|
||||
dbtMashConfig,
|
||||
)
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, List, Dict, Union, Any
|
||||
from typing import Optional, List, Dict, Union, Any, ClassVar
|
||||
from typing_extensions import Annotated
|
||||
from mashumaro.types import SerializableType
|
||||
from mashumaro.jsonschema.annotations import Pattern
|
||||
|
||||
|
||||
DEFAULT_SEND_ANONYMOUS_USAGE_STATS = True
|
||||
@@ -25,12 +26,8 @@ class SemverString(str, SerializableType):
|
||||
return SemverString(value)
|
||||
|
||||
|
||||
# this supports full semver,
|
||||
# but also allows for 2 group version numbers, (allows '1.0').
|
||||
register_pattern(
|
||||
SemverString,
|
||||
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)?$", # noqa
|
||||
)
|
||||
# This supports full semver, but also allows for 2 group version numbers, (allows '1.0').
|
||||
sem_ver_pattern = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)?$"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -42,13 +39,14 @@ class Quoting(dbtClassMixin, Mergeable):
|
||||
|
||||
|
||||
@dataclass
|
||||
class Package(Replaceable, HyphenatedDbtClassMixin):
|
||||
class Package(dbtClassMixin, Replaceable):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalPackage(Package):
|
||||
local: str
|
||||
unrendered: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# `float` also allows `int`, according to PEP484 (and jsonschema!)
|
||||
@@ -59,14 +57,16 @@ RawVersion = Union[str, float]
|
||||
class TarballPackage(Package):
|
||||
tarball: str
|
||||
name: str
|
||||
unrendered: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GitPackage(Package):
|
||||
git: str
|
||||
revision: Optional[RawVersion] = None
|
||||
warn_unpinned: Optional[bool] = None
|
||||
warn_unpinned: Optional[bool] = field(default=None, metadata={"alias": "warn-unpinned"})
|
||||
subdirectory: Optional[str] = None
|
||||
unrendered: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def get_revisions(self) -> List[str]:
|
||||
if self.revision is None:
|
||||
@@ -80,6 +80,7 @@ class RegistryPackage(Package):
|
||||
package: str
|
||||
version: Union[RawVersion, List[RawVersion]]
|
||||
install_prerelease: Optional[bool] = False
|
||||
unrendered: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def get_versions(self) -> List[str]:
|
||||
if isinstance(self.version, list):
|
||||
@@ -182,10 +183,13 @@ BANNED_PROJECT_NAMES = {
|
||||
|
||||
|
||||
@dataclass
|
||||
class Project(HyphenatedDbtClassMixin, Replaceable):
|
||||
name: Identifier
|
||||
class Project(dbtClassMixin, Replaceable):
|
||||
_hyphenated: ClassVar[bool] = True
|
||||
# Annotated is used by mashumaro for jsonschema generation
|
||||
name: Annotated[Identifier, Pattern(r"^[^\d\W]\w*$")]
|
||||
config_version: Optional[int] = 2
|
||||
version: Optional[Union[SemverString, float]] = None
|
||||
# Annotated is used by mashumaro for jsonschema generation
|
||||
version: Optional[Union[Annotated[SemverString, Pattern(sem_ver_pattern)], float]] = None
|
||||
project_root: Optional[str] = None
|
||||
source_paths: Optional[List[str]] = None
|
||||
model_paths: Optional[List[str]] = None
|
||||
@@ -214,6 +218,8 @@ class Project(HyphenatedDbtClassMixin, Replaceable):
|
||||
sources: Dict[str, Any] = field(default_factory=dict)
|
||||
tests: Dict[str, Any] = field(default_factory=dict)
|
||||
metrics: Dict[str, Any] = field(default_factory=dict)
|
||||
semantic_models: Dict[str, Any] = field(default_factory=dict)
|
||||
saved_queries: Dict[str, Any] = field(default_factory=dict)
|
||||
exposures: Dict[str, Any] = field(default_factory=dict)
|
||||
vars: Optional[Dict[str, Any]] = field(
|
||||
default=None,
|
||||
@@ -224,6 +230,36 @@ class Project(HyphenatedDbtClassMixin, Replaceable):
|
||||
packages: List[PackageSpec] = field(default_factory=list)
|
||||
query_comment: Optional[Union[QueryComment, NoValue, str]] = field(default_factory=NoValue)
|
||||
restrict_access: bool = False
|
||||
dbt_cloud: Optional[Dict[str, Any]] = None
|
||||
|
||||
class Config(dbtMashConfig):
|
||||
# These tell mashumaro to use aliases for jsonschema and for "from_dict"
|
||||
aliases = {
|
||||
"config_version": "config-version",
|
||||
"project_root": "project-root",
|
||||
"source_paths": "source-paths",
|
||||
"model_paths": "model-paths",
|
||||
"macro_paths": "macro-paths",
|
||||
"data_paths": "data-paths",
|
||||
"seed_paths": "seed-paths",
|
||||
"test_paths": "test-paths",
|
||||
"analysis_paths": "analysis-paths",
|
||||
"docs_paths": "docs-paths",
|
||||
"asset_paths": "asset-paths",
|
||||
"target_path": "target-path",
|
||||
"snapshot_paths": "snapshot-paths",
|
||||
"clean_targets": "clean-targets",
|
||||
"log_path": "log-path",
|
||||
"packages_install_path": "packages-install-path",
|
||||
"on_run_start": "on-run-start",
|
||||
"on_run_end": "on-run-end",
|
||||
"require_dbt_version": "require-dbt-version",
|
||||
"query_comment": "query-comment",
|
||||
"restrict_access": "restrict-access",
|
||||
"semantic_models": "semantic-models",
|
||||
"saved_queries": "saved-queries",
|
||||
"dbt_cloud": "dbt-cloud",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data):
|
||||
@@ -240,6 +276,10 @@ class Project(HyphenatedDbtClassMixin, Replaceable):
|
||||
or not isinstance(entry["search_order"], list)
|
||||
):
|
||||
raise ValidationError(f"Invalid project dispatch config: {entry}")
|
||||
if "dbt_cloud" in data and not isinstance(data["dbt_cloud"], dict):
|
||||
raise ValidationError(
|
||||
f"Invalid dbt_cloud config. Expected a 'dict' but got '{type(data['dbt_cloud'])}'"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -267,10 +307,10 @@ class UserConfig(ExtensibleDbtClassMixin, Replaceable, UserConfigContract):
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfileConfig(HyphenatedDbtClassMixin, Replaceable):
|
||||
profile_name: str = field(metadata={"preserve_underscore": True})
|
||||
target_name: str = field(metadata={"preserve_underscore": True})
|
||||
user_config: UserConfig = field(metadata={"preserve_underscore": True})
|
||||
class ProfileConfig(dbtClassMixin, Replaceable):
|
||||
profile_name: str
|
||||
target_name: str
|
||||
user_config: UserConfig
|
||||
threads: int
|
||||
# TODO: make this a dynamic union of some kind?
|
||||
credentials: Optional[Dict[str, Any]]
|
||||
|
||||
@@ -19,6 +19,7 @@ class RelationType(StrEnum):
|
||||
CTE = "cte"
|
||||
MaterializedView = "materialized_view"
|
||||
External = "external"
|
||||
Ephemeral = "ephemeral"
|
||||
|
||||
|
||||
class ComponentName(StrEnum):
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import threading
|
||||
|
||||
from dbt.contracts.graph.unparsed import FreshnessThreshold
|
||||
from dbt.contracts.graph.nodes import SourceDefinition, ResultNode
|
||||
from dbt.contracts.graph.nodes import CompiledNode, SourceDefinition, ResultNode
|
||||
from dbt.contracts.util import (
|
||||
BaseArtifactMetadata,
|
||||
ArtifactMixin,
|
||||
VersionedSchema,
|
||||
Replaceable,
|
||||
schema_version,
|
||||
get_artifact_schema_version,
|
||||
)
|
||||
from dbt.exceptions import DbtInternalError
|
||||
from dbt.events.functions import fire_event
|
||||
@@ -31,6 +32,8 @@ from typing import (
|
||||
Optional,
|
||||
Sequence,
|
||||
Union,
|
||||
Iterable,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from dbt.clients.system import write_json
|
||||
@@ -59,7 +62,7 @@ class TimingInfo(dbtClassMixin):
|
||||
|
||||
# This is a context manager
|
||||
class collect_timing_info:
|
||||
def __init__(self, name: str, callback: Callable[[TimingInfo], None]):
|
||||
def __init__(self, name: str, callback: Callable[[TimingInfo], None]) -> None:
|
||||
self.timing_info = TimingInfo(name=name)
|
||||
self.callback = callback
|
||||
|
||||
@@ -203,9 +206,15 @@ class RunResultsMetadata(BaseArtifactMetadata):
|
||||
@dataclass
|
||||
class RunResultOutput(BaseResult):
|
||||
unique_id: str
|
||||
compiled: Optional[bool]
|
||||
compiled_code: Optional[str]
|
||||
relation_name: Optional[str]
|
||||
|
||||
|
||||
def process_run_result(result: RunResult) -> RunResultOutput:
|
||||
|
||||
compiled = isinstance(result.node, CompiledNode)
|
||||
|
||||
return RunResultOutput(
|
||||
unique_id=result.node.unique_id,
|
||||
status=result.status,
|
||||
@@ -215,6 +224,9 @@ def process_run_result(result: RunResult) -> RunResultOutput:
|
||||
message=result.message,
|
||||
adapter_response=result.adapter_response,
|
||||
failures=result.failures,
|
||||
compiled=result.node.compiled if compiled else None, # type:ignore
|
||||
compiled_code=result.node.compiled_code if compiled else None, # type:ignore
|
||||
relation_name=result.node.relation_name if compiled else None, # type:ignore
|
||||
)
|
||||
|
||||
|
||||
@@ -237,7 +249,7 @@ class RunExecutionResult(
|
||||
|
||||
|
||||
@dataclass
|
||||
@schema_version("run-results", 4)
|
||||
@schema_version("run-results", 5)
|
||||
class RunResultsArtifact(ExecutionResult, ArtifactMixin):
|
||||
results: Sequence[RunResultOutput]
|
||||
args: Dict[str, Any] = field(default_factory=dict)
|
||||
@@ -259,6 +271,27 @@ class RunResultsArtifact(ExecutionResult, ArtifactMixin):
|
||||
)
|
||||
return cls(metadata=meta, results=processed_results, elapsed_time=elapsed_time, args=args)
|
||||
|
||||
@classmethod
|
||||
def compatible_previous_versions(cls) -> Iterable[Tuple[str, int]]:
|
||||
return [
|
||||
("run-results", 4),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def upgrade_schema_version(cls, data):
|
||||
"""This overrides the "upgrade_schema_version" call in VersionedSchema (via
|
||||
ArtifactMixin) to modify the dictionary passed in from earlier versions of the run_results."""
|
||||
run_results_schema_version = get_artifact_schema_version(data)
|
||||
# If less than the current version (v5), preprocess contents to match latest schema version
|
||||
if run_results_schema_version <= 5:
|
||||
# In v5, we added 'compiled' attributes to each result entry
|
||||
# Going forward, dbt expects these to be populated
|
||||
for result in data["results"]:
|
||||
result["compiled"] = False
|
||||
result["compiled_code"] = ""
|
||||
result["relation_name"] = ""
|
||||
return cls.from_dict(data)
|
||||
|
||||
def write(self, path: str):
|
||||
write_json(path, self.to_dict(omit_none=False))
|
||||
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
from pathlib import Path
|
||||
from .graph.manifest import WritableManifest
|
||||
from .results import RunResultsArtifact
|
||||
from .results import FreshnessExecutionResultArtifact
|
||||
from typing import Optional
|
||||
|
||||
from dbt.contracts.graph.manifest import WritableManifest
|
||||
from dbt.contracts.results import FreshnessExecutionResultArtifact
|
||||
from dbt.contracts.results import RunResultsArtifact
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.types import WarnStateTargetEqual
|
||||
from dbt.exceptions import IncompatibleSchemaError
|
||||
|
||||
|
||||
def load_result_state(results_path) -> Optional[RunResultsArtifact]:
|
||||
if results_path.exists() and results_path.is_file():
|
||||
try:
|
||||
return RunResultsArtifact.read_and_check_versions(str(results_path))
|
||||
except IncompatibleSchemaError as exc:
|
||||
exc.add_filename(str(results_path))
|
||||
raise
|
||||
return None
|
||||
|
||||
|
||||
class PreviousState:
|
||||
def __init__(self, state_path: Path, target_path: Path, project_root: Path):
|
||||
def __init__(self, state_path: Path, target_path: Path, project_root: Path) -> None:
|
||||
self.state_path: Path = state_path
|
||||
self.target_path: Path = target_path
|
||||
self.project_root: Path = project_root
|
||||
@@ -16,6 +29,9 @@ class PreviousState:
|
||||
self.sources: Optional[FreshnessExecutionResultArtifact] = None
|
||||
self.sources_current: Optional[FreshnessExecutionResultArtifact] = None
|
||||
|
||||
if self.state_path == self.target_path:
|
||||
fire_event(WarnStateTargetEqual(state_path=str(self.state_path)))
|
||||
|
||||
# Note: if state_path is absolute, project_root will be ignored.
|
||||
manifest_path = self.project_root / self.state_path / "manifest.json"
|
||||
if manifest_path.exists() and manifest_path.is_file():
|
||||
@@ -26,12 +42,7 @@ class PreviousState:
|
||||
raise
|
||||
|
||||
results_path = self.project_root / self.state_path / "run_results.json"
|
||||
if results_path.exists() and results_path.is_file():
|
||||
try:
|
||||
self.results = RunResultsArtifact.read_and_check_versions(str(results_path))
|
||||
except IncompatibleSchemaError as exc:
|
||||
exc.add_filename(str(results_path))
|
||||
raise
|
||||
self.results = load_result_state(results_path)
|
||||
|
||||
sources_path = self.project_root / self.state_path / "sources.json"
|
||||
if sources_path.exists() and sources_path.is_file():
|
||||
|
||||
@@ -16,8 +16,10 @@ from dbt.dataclass_schema import dbtClassMixin
|
||||
from dbt.dataclass_schema import (
|
||||
ValidatedStringMixin,
|
||||
ValidationError,
|
||||
register_pattern,
|
||||
)
|
||||
from mashumaro.jsonschema import build_json_schema
|
||||
from mashumaro.jsonschema.dialects import DRAFT_2020_12
|
||||
import functools
|
||||
|
||||
|
||||
SourceKey = Tuple[str, str]
|
||||
@@ -90,7 +92,9 @@ class AdditionalPropertiesMixin:
|
||||
cls_keys = cls._get_field_names()
|
||||
new_dict = {}
|
||||
for key, value in data.items():
|
||||
if key not in cls_keys and key != "_extra":
|
||||
# The pre-hook/post-hook mess hasn't been converted yet... That happens in
|
||||
# the super().__pre_deserialize__ below...
|
||||
if key not in cls_keys and key not in ["_extra", "pre-hook", "post-hook"]:
|
||||
if "_extra" not in new_dict:
|
||||
new_dict["_extra"] = {}
|
||||
new_dict["_extra"][key] = value
|
||||
@@ -192,11 +196,12 @@ class VersionedSchema(dbtClassMixin):
|
||||
dbt_schema_version: ClassVar[SchemaVersion]
|
||||
|
||||
@classmethod
|
||||
def json_schema(cls, embeddable: bool = False) -> Dict[str, Any]:
|
||||
result = super().json_schema(embeddable=embeddable)
|
||||
if not embeddable:
|
||||
result["$id"] = str(cls.dbt_schema_version)
|
||||
return result
|
||||
@functools.lru_cache
|
||||
def json_schema(cls) -> Dict[str, Any]:
|
||||
json_schema_obj = build_json_schema(cls, dialect=DRAFT_2020_12, with_dialect_uri=True)
|
||||
json_schema = json_schema_obj.to_dict()
|
||||
json_schema["$id"] = str(cls.dbt_schema_version)
|
||||
return json_schema
|
||||
|
||||
@classmethod
|
||||
def is_compatible_version(cls, schema_version):
|
||||
@@ -257,7 +262,29 @@ class ArtifactMixin(VersionedSchema, Writable, Readable):
|
||||
raise DbtInternalError("Cannot call from_dict with no schema version!")
|
||||
|
||||
|
||||
def get_artifact_schema_version(dct: dict) -> int:
|
||||
schema_version = dct.get("metadata", {}).get("dbt_schema_version", None)
|
||||
if not schema_version:
|
||||
raise ValueError("Artifact is missing schema version")
|
||||
|
||||
# schema_version is in this format: https://schemas.getdbt.com/dbt/manifest/v10.json
|
||||
# What the code below is doing:
|
||||
# 1. Split on "/" – v10.json
|
||||
# 2. Split on "." – v10
|
||||
# 3. Skip first character – 10
|
||||
# 4. Convert to int
|
||||
# TODO: If this gets more complicated, turn into a regex
|
||||
return int(schema_version.split("/")[-1].split(".")[0][1:])
|
||||
|
||||
|
||||
class Identifier(ValidatedStringMixin):
|
||||
"""Our definition of a valid Identifier is the same as what's valid for an unquoted database table name.
|
||||
|
||||
That is:
|
||||
1. It can contain a-z, A-Z, 0-9, and _
|
||||
1. It cannot start with a number
|
||||
"""
|
||||
|
||||
ValidationRegex = r"^[^\d\W]\w*$"
|
||||
|
||||
@classmethod
|
||||
@@ -271,6 +298,3 @@ class Identifier(ValidatedStringMixin):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
register_pattern(Identifier, r"^[^\d\W]\w*$")
|
||||
|
||||
@@ -1,97 +1,123 @@
|
||||
from typing import (
|
||||
Type,
|
||||
ClassVar,
|
||||
cast,
|
||||
)
|
||||
from typing import ClassVar, cast, get_type_hints, List, Tuple, Dict, Any, Optional
|
||||
import re
|
||||
from dataclasses import fields
|
||||
import jsonschema
|
||||
from dataclasses import fields, Field
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
from dateutil.parser import parse
|
||||
|
||||
from hologram import JsonSchemaMixin, FieldEncoder, ValidationError
|
||||
|
||||
# type: ignore
|
||||
from mashumaro import DataClassDictMixin
|
||||
from mashumaro.config import TO_DICT_ADD_OMIT_NONE_FLAG, BaseConfig as MashBaseConfig
|
||||
from mashumaro.types import SerializableType, SerializationStrategy
|
||||
from mashumaro.jsonschema import build_json_schema
|
||||
|
||||
import functools
|
||||
|
||||
|
||||
class ValidationError(jsonschema.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class DateTimeSerialization(SerializationStrategy):
|
||||
def serialize(self, value):
|
||||
def serialize(self, value) -> str:
|
||||
out = value.isoformat()
|
||||
# Assume UTC if timezone is missing
|
||||
if value.tzinfo is None:
|
||||
out += "Z"
|
||||
return out
|
||||
|
||||
def deserialize(self, value):
|
||||
def deserialize(self, value) -> datetime:
|
||||
return value if isinstance(value, datetime) else parse(cast(str, value))
|
||||
|
||||
|
||||
# This class pulls in both JsonSchemaMixin from Hologram and
|
||||
# DataClassDictMixin from our fork of Mashumaro. The 'to_dict'
|
||||
# and 'from_dict' methods come from Mashumaro. Building
|
||||
# jsonschemas for every class and the 'validate' method
|
||||
# come from Hologram.
|
||||
class dbtClassMixin(DataClassDictMixin, JsonSchemaMixin):
|
||||
class dbtMashConfig(MashBaseConfig):
|
||||
code_generation_options = [
|
||||
TO_DICT_ADD_OMIT_NONE_FLAG,
|
||||
]
|
||||
serialization_strategy = {
|
||||
datetime: DateTimeSerialization(),
|
||||
}
|
||||
json_schema = {
|
||||
"additionalProperties": False,
|
||||
}
|
||||
serialize_by_alias = True
|
||||
|
||||
|
||||
# This class pulls in DataClassDictMixin from Mashumaro. The 'to_dict'
|
||||
# and 'from_dict' methods come from Mashumaro.
|
||||
class dbtClassMixin(DataClassDictMixin):
|
||||
"""The Mixin adds methods to generate a JSON schema and
|
||||
convert to and from JSON encodable dicts with validation
|
||||
against the schema
|
||||
"""
|
||||
|
||||
class Config(MashBaseConfig):
|
||||
code_generation_options = [
|
||||
TO_DICT_ADD_OMIT_NONE_FLAG,
|
||||
]
|
||||
serialization_strategy = {
|
||||
datetime: DateTimeSerialization(),
|
||||
}
|
||||
_mapped_fields: ClassVar[Optional[Dict[Any, List[Tuple[Field, str]]]]] = None
|
||||
|
||||
# Config class used by Mashumaro
|
||||
class Config(dbtMashConfig):
|
||||
pass
|
||||
|
||||
_hyphenated: ClassVar[bool] = False
|
||||
ADDITIONAL_PROPERTIES: ClassVar[bool] = False
|
||||
|
||||
# This is called by the mashumaro to_dict in order to handle
|
||||
# nested classes.
|
||||
# Munges the dict that's returned.
|
||||
def __post_serialize__(self, dct):
|
||||
if self._hyphenated:
|
||||
new_dict = {}
|
||||
for key in dct:
|
||||
if "_" in key:
|
||||
new_key = key.replace("_", "-")
|
||||
new_dict[new_key] = dct[key]
|
||||
else:
|
||||
new_dict[key] = dct[key]
|
||||
dct = new_dict
|
||||
|
||||
return dct
|
||||
|
||||
# This is called by the mashumaro _from_dict method, before
|
||||
# performing the conversion to a dict
|
||||
# This is called by the mashumaro from_dict in order to handle
|
||||
# nested classes. We no longer do any munging here, but leaving here
|
||||
# so that subclasses can leave super() in place for possible future needs.
|
||||
@classmethod
|
||||
def __pre_deserialize__(cls, data):
|
||||
# `data` might not be a dict, e.g. for `query_comment`, which accepts
|
||||
# a dict or a string; only snake-case for dict values.
|
||||
if cls._hyphenated and isinstance(data, dict):
|
||||
new_dict = {}
|
||||
for key in data:
|
||||
if "-" in key:
|
||||
new_key = key.replace("-", "_")
|
||||
new_dict[new_key] = data[key]
|
||||
else:
|
||||
new_dict[key] = data[key]
|
||||
data = new_dict
|
||||
return data
|
||||
|
||||
# This is used in the hologram._encode_field method, which calls
|
||||
# a 'to_dict' method which does not have the same parameters in
|
||||
# hologram and in mashumaro.
|
||||
def _local_to_dict(self, **kwargs):
|
||||
args = {}
|
||||
if "omit_none" in kwargs:
|
||||
args["omit_none"] = kwargs["omit_none"]
|
||||
return self.to_dict(**args)
|
||||
# This is called by the mashumaro to_dict in order to handle
|
||||
# nested classes. We no longer do any munging here, but leaving here
|
||||
# so that subclasses can leave super() in place for possible future needs.
|
||||
def __post_serialize__(self, data):
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
@functools.lru_cache
|
||||
def json_schema(cls):
|
||||
json_schema_obj = build_json_schema(cls)
|
||||
json_schema = json_schema_obj.to_dict()
|
||||
return json_schema
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data):
|
||||
json_schema = cls.json_schema()
|
||||
validator = jsonschema.Draft7Validator(json_schema)
|
||||
error = next(iter(validator.iter_errors(data)), None)
|
||||
if error is not None:
|
||||
raise ValidationError.create_from(error) from error
|
||||
|
||||
# This method was copied from hologram. Used in model_config.py and relation.py
|
||||
@classmethod
|
||||
def _get_fields(cls) -> List[Tuple[Field, str]]:
|
||||
if cls._mapped_fields is None:
|
||||
cls._mapped_fields = {}
|
||||
if cls.__name__ not in cls._mapped_fields:
|
||||
mapped_fields = []
|
||||
type_hints = get_type_hints(cls)
|
||||
|
||||
for f in fields(cls): # type: ignore
|
||||
# Skip internal fields
|
||||
if f.name.startswith("_"):
|
||||
continue
|
||||
|
||||
# Note fields() doesn't resolve forward refs
|
||||
f.type = type_hints[f.name]
|
||||
|
||||
# hologram used the "field_mapping" here, but we use the
|
||||
# the field's metadata "alias". Since this method is mainly
|
||||
# just used in merging config dicts, it mostly applies to
|
||||
# pre-hook and post-hook.
|
||||
field_name = f.metadata.get("alias", f.name)
|
||||
mapped_fields.append((f, field_name))
|
||||
cls._mapped_fields[cls.__name__] = mapped_fields
|
||||
return cls._mapped_fields[cls.__name__]
|
||||
|
||||
# copied from hologram. Used in tests
|
||||
@classmethod
|
||||
def _get_field_names(cls):
|
||||
return [element[1] for element in cls._get_fields()]
|
||||
|
||||
|
||||
class ValidatedStringMixin(str, SerializableType):
|
||||
@@ -130,38 +156,10 @@ class StrEnum(str, SerializableType, Enum):
|
||||
return cls(value)
|
||||
|
||||
|
||||
class HyphenatedDbtClassMixin(dbtClassMixin):
|
||||
# used by from_dict/to_dict
|
||||
_hyphenated: ClassVar[bool] = True
|
||||
|
||||
# used by jsonschema validation, _get_fields
|
||||
@classmethod
|
||||
def field_mapping(cls):
|
||||
result = {}
|
||||
for field in fields(cls):
|
||||
skip = field.metadata.get("preserve_underscore")
|
||||
if skip:
|
||||
continue
|
||||
|
||||
if "_" in field.name:
|
||||
result[field.name] = field.name.replace("_", "-")
|
||||
return result
|
||||
|
||||
|
||||
class ExtensibleDbtClassMixin(dbtClassMixin):
|
||||
ADDITIONAL_PROPERTIES = True
|
||||
|
||||
|
||||
# This is used by Hologram in jsonschema validation
|
||||
def register_pattern(base_type: Type, pattern: str) -> None:
|
||||
"""base_type should be a typing.NewType that should always have the given
|
||||
regex pattern. That means that its underlying type ('__supertype__') had
|
||||
better be a str!
|
||||
"""
|
||||
|
||||
class PatternEncoder(FieldEncoder):
|
||||
@property
|
||||
def json_schema(self):
|
||||
return {"type": "string", "pattern": pattern}
|
||||
|
||||
dbtClassMixin.register_field_encoders({base_type: PatternEncoder()})
|
||||
class Config(dbtMashConfig):
|
||||
json_schema = {
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import functools
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Generic, TypeVar
|
||||
from typing import List, Optional, Generic, TypeVar, Dict
|
||||
|
||||
from dbt.clients import system
|
||||
from dbt.contracts.project import ProjectPackageMetadata
|
||||
@@ -84,6 +84,10 @@ class PinnedPackage(BasePackage):
|
||||
def nice_version_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
raise NotImplementedError
|
||||
|
||||
def fetch_metadata(self, project, renderer):
|
||||
if not self._cached_metadata:
|
||||
self._cached_metadata = self._fetch_metadata(project, renderer)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
from dbt.clients import git, system
|
||||
from dbt.config.project import PartialProject, Project
|
||||
@@ -9,9 +9,9 @@ from dbt.contracts.project import (
|
||||
GitPackage,
|
||||
)
|
||||
from dbt.deps.base import PinnedPackage, UnpinnedPackage, get_downloads_path
|
||||
from dbt.exceptions import ExecutableError, MultipleVersionGitDepsError
|
||||
from dbt.exceptions import ExecutableError, MultipleVersionGitDepsError, scrub_secrets, env_secrets
|
||||
from dbt.events.functions import fire_event, warn_or_error
|
||||
from dbt.events.types import EnsureGitInstalled, DepsUnpinned
|
||||
from dbt.events.types import EnsureGitInstalled, DepsUnpinned, DepsScrubbedPackageName
|
||||
from dbt.utils import md5
|
||||
|
||||
|
||||
@@ -20,13 +20,20 @@ def md5sum(s: str):
|
||||
|
||||
|
||||
class GitPackageMixin:
|
||||
def __init__(self, git: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
git: str,
|
||||
git_unrendered: str,
|
||||
subdirectory: Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.git = git
|
||||
self.git_unrendered = git_unrendered
|
||||
self.subdirectory = subdirectory
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.git
|
||||
return f"{self.git}/{self.subdirectory}" if self.subdirectory else self.git
|
||||
|
||||
def source_type(self) -> str:
|
||||
return "git"
|
||||
@@ -36,15 +43,28 @@ class GitPinnedPackage(GitPackageMixin, PinnedPackage):
|
||||
def __init__(
|
||||
self,
|
||||
git: str,
|
||||
git_unrendered: str,
|
||||
revision: str,
|
||||
warn_unpinned: bool = True,
|
||||
subdirectory: Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__(git)
|
||||
super().__init__(git, git_unrendered, subdirectory)
|
||||
self.revision = revision
|
||||
self.warn_unpinned = warn_unpinned
|
||||
self.subdirectory = subdirectory
|
||||
self._checkout_name = md5sum(self.git)
|
||||
self._checkout_name = md5sum(self.name)
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
git_scrubbed = scrub_secrets(self.git_unrendered, env_secrets())
|
||||
if self.git_unrendered != git_scrubbed:
|
||||
warn_or_error(DepsScrubbedPackageName(package_name=git_scrubbed))
|
||||
ret = {
|
||||
"git": git_scrubbed,
|
||||
"revision": self.revision,
|
||||
}
|
||||
if self.subdirectory:
|
||||
ret["subdirectory"] = self.subdirectory
|
||||
return ret
|
||||
|
||||
def get_version(self):
|
||||
return self.revision
|
||||
@@ -82,8 +102,13 @@ class GitPinnedPackage(GitPackageMixin, PinnedPackage):
|
||||
) -> ProjectPackageMetadata:
|
||||
path = self._checkout()
|
||||
|
||||
# raise warning (or error) if this package is not pinned
|
||||
if (self.revision == "HEAD" or self.revision in ("main", "master")) and self.warn_unpinned:
|
||||
warn_or_error(DepsUnpinned(git=self.git))
|
||||
warn_or_error(DepsUnpinned(revision=self.revision, git=self.git))
|
||||
|
||||
# now overwrite 'revision' with actual commit SHA
|
||||
self.revision = git.get_current_sha(path)
|
||||
|
||||
partial = PartialProject.from_project_root(path)
|
||||
return partial.render_package_metadata(renderer)
|
||||
|
||||
@@ -102,11 +127,12 @@ class GitUnpinnedPackage(GitPackageMixin, UnpinnedPackage[GitPinnedPackage]):
|
||||
def __init__(
|
||||
self,
|
||||
git: str,
|
||||
git_unrendered: str,
|
||||
revisions: List[str],
|
||||
warn_unpinned: bool = True,
|
||||
subdirectory: Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__(git)
|
||||
super().__init__(git, git_unrendered, subdirectory)
|
||||
self.revisions = revisions
|
||||
self.warn_unpinned = warn_unpinned
|
||||
self.subdirectory = subdirectory
|
||||
@@ -119,6 +145,7 @@ class GitUnpinnedPackage(GitPackageMixin, UnpinnedPackage[GitPinnedPackage]):
|
||||
warn_unpinned = contract.warn_unpinned is not False
|
||||
return cls(
|
||||
git=contract.git,
|
||||
git_unrendered=(contract.unrendered.get("git") or contract.git),
|
||||
revisions=revisions,
|
||||
warn_unpinned=warn_unpinned,
|
||||
subdirectory=contract.subdirectory,
|
||||
@@ -129,13 +156,21 @@ class GitUnpinnedPackage(GitPackageMixin, UnpinnedPackage[GitPinnedPackage]):
|
||||
other = self.git[:-4]
|
||||
else:
|
||||
other = self.git + ".git"
|
||||
return [self.git, other]
|
||||
|
||||
if self.subdirectory:
|
||||
git_name = f"{self.git}/{self.subdirectory}"
|
||||
other = f"{other}/{self.subdirectory}"
|
||||
else:
|
||||
git_name = self.git
|
||||
|
||||
return [git_name, other]
|
||||
|
||||
def incorporate(self, other: "GitUnpinnedPackage") -> "GitUnpinnedPackage":
|
||||
warn_unpinned = self.warn_unpinned and other.warn_unpinned
|
||||
|
||||
return GitUnpinnedPackage(
|
||||
git=self.git,
|
||||
git_unrendered=self.git_unrendered,
|
||||
revisions=self.revisions + other.revisions,
|
||||
warn_unpinned=warn_unpinned,
|
||||
subdirectory=self.subdirectory,
|
||||
@@ -146,10 +181,10 @@ class GitUnpinnedPackage(GitPackageMixin, UnpinnedPackage[GitPinnedPackage]):
|
||||
if len(requested) == 0:
|
||||
requested = {"HEAD"}
|
||||
elif len(requested) > 1:
|
||||
raise MultipleVersionGitDepsError(self.git, requested)
|
||||
|
||||
raise MultipleVersionGitDepsError(self.name, requested)
|
||||
return GitPinnedPackage(
|
||||
git=self.git,
|
||||
git_unrendered=self.git_unrendered,
|
||||
revision=requested.pop(),
|
||||
warn_unpinned=self.warn_unpinned,
|
||||
subdirectory=self.subdirectory,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import shutil
|
||||
from typing import Dict
|
||||
|
||||
from dbt.clients import system
|
||||
from dbt.deps.base import PinnedPackage, UnpinnedPackage
|
||||
@@ -29,6 +30,11 @@ class LocalPinnedPackage(LocalPackageMixin, PinnedPackage):
|
||||
def __init__(self, local: str) -> None:
|
||||
super().__init__(local)
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return {
|
||||
"local": self.local,
|
||||
}
|
||||
|
||||
def get_version(self):
|
||||
return None
|
||||
|
||||
@@ -51,19 +57,15 @@ class LocalPinnedPackage(LocalPackageMixin, PinnedPackage):
|
||||
src_path = self.resolve_path(project)
|
||||
dest_path = self.get_installation_path(project, renderer)
|
||||
|
||||
can_create_symlink = system.supports_symlinks()
|
||||
|
||||
if system.path_exists(dest_path):
|
||||
if not system.path_is_symlink(dest_path):
|
||||
system.rmdir(dest_path)
|
||||
else:
|
||||
system.remove_file(dest_path)
|
||||
|
||||
if can_create_symlink:
|
||||
try:
|
||||
fire_event(DepsCreatingLocalSymlink())
|
||||
system.make_symlink(src_path, dest_path)
|
||||
|
||||
else:
|
||||
except OSError:
|
||||
fire_event(DepsSymlinkNotAvailable())
|
||||
shutil.copytree(src_path, dest_path)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
|
||||
from dbt import semver
|
||||
from dbt.flags import get_flags
|
||||
@@ -40,6 +40,12 @@ class RegistryPinnedPackage(RegistryPackageMixin, PinnedPackage):
|
||||
def name(self):
|
||||
return self.package
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return {
|
||||
"package": self.package,
|
||||
"version": self.version,
|
||||
}
|
||||
|
||||
def source_type(self):
|
||||
return "hub"
|
||||
|
||||
|
||||
@@ -135,3 +135,15 @@ def resolve_packages(
|
||||
resolved = final.resolved()
|
||||
_check_for_duplicate_project_names(resolved, project, renderer)
|
||||
return resolved
|
||||
|
||||
|
||||
def resolve_lock_packages(packages: List[PackageContract]) -> List[PinnedPackage]:
|
||||
lock_packages = PackageListing.from_contracts(packages)
|
||||
final = PackageListing()
|
||||
|
||||
for package in lock_packages:
|
||||
final.incorporate(package)
|
||||
|
||||
resolved = final.resolved()
|
||||
|
||||
return resolved
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
from typing import Dict
|
||||
|
||||
from dbt.contracts.project import RegistryPackageMetadata, TarballPackage
|
||||
from dbt.deps.base import PinnedPackage, UnpinnedPackage
|
||||
from dbt.exceptions import scrub_secrets, env_secrets
|
||||
from dbt.events.functions import warn_or_error
|
||||
from dbt.events.types import DepsScrubbedPackageName
|
||||
|
||||
|
||||
class TarballPackageMixin:
|
||||
def __init__(self, tarball: str) -> None:
|
||||
def __init__(self, tarball: str, tarball_unrendered: str) -> None:
|
||||
super().__init__()
|
||||
self.tarball = tarball
|
||||
self.tarball_unrendered = tarball_unrendered
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -16,8 +22,8 @@ class TarballPackageMixin:
|
||||
|
||||
|
||||
class TarballPinnedPackage(TarballPackageMixin, PinnedPackage):
|
||||
def __init__(self, tarball: str, package: str) -> None:
|
||||
super().__init__(tarball)
|
||||
def __init__(self, tarball: str, tarball_unrendered: str, package: str) -> None:
|
||||
super().__init__(tarball, tarball_unrendered)
|
||||
# setup to recycle RegistryPinnedPackage fns
|
||||
self.package = package
|
||||
self.version = "tarball"
|
||||
@@ -26,6 +32,15 @@ class TarballPinnedPackage(TarballPackageMixin, PinnedPackage):
|
||||
def name(self):
|
||||
return self.package
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
tarball_scrubbed = scrub_secrets(self.tarball_unrendered, env_secrets())
|
||||
if self.tarball_unrendered != tarball_scrubbed:
|
||||
warn_or_error(DepsScrubbedPackageName(package_name=tarball_scrubbed))
|
||||
return {
|
||||
"tarball": tarball_scrubbed,
|
||||
"name": self.package,
|
||||
}
|
||||
|
||||
def get_version(self):
|
||||
return self.version
|
||||
|
||||
@@ -56,19 +71,28 @@ class TarballUnpinnedPackage(TarballPackageMixin, UnpinnedPackage[TarballPinnedP
|
||||
def __init__(
|
||||
self,
|
||||
tarball: str,
|
||||
tarball_unrendered: str,
|
||||
package: str,
|
||||
) -> None:
|
||||
super().__init__(tarball)
|
||||
super().__init__(tarball, tarball_unrendered)
|
||||
# setup to recycle RegistryPinnedPackage fns
|
||||
self.package = package
|
||||
self.version = "tarball"
|
||||
|
||||
@classmethod
|
||||
def from_contract(cls, contract: TarballPackage) -> "TarballUnpinnedPackage":
|
||||
return cls(tarball=contract.tarball, package=contract.name)
|
||||
return cls(
|
||||
tarball=contract.tarball,
|
||||
tarball_unrendered=(contract.unrendered.get("tarball") or contract.tarball),
|
||||
package=contract.name,
|
||||
)
|
||||
|
||||
def incorporate(self, other: "TarballUnpinnedPackage") -> "TarballUnpinnedPackage":
|
||||
return TarballUnpinnedPackage(tarball=self.tarball, package=self.package)
|
||||
return TarballUnpinnedPackage(
|
||||
tarball=self.tarball, tarball_unrendered=self.tarball_unrendered, package=self.package
|
||||
)
|
||||
|
||||
def resolved(self) -> TarballPinnedPackage:
|
||||
return TarballPinnedPackage(tarball=self.tarball, package=self.package)
|
||||
return TarballPinnedPackage(
|
||||
tarball=self.tarball, tarball_unrendered=self.tarball_unrendered, package=self.package
|
||||
)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import traceback
|
||||
from dataclasses import dataclass
|
||||
from dbt.events.functions import fire_event
|
||||
from dbt.events.functions import fire_event, EVENT_MANAGER
|
||||
from dbt.events.contextvars import get_node_info
|
||||
from dbt.events.event_handler import set_package_logging
|
||||
from dbt.events.types import (
|
||||
AdapterEventDebug,
|
||||
AdapterEventInfo,
|
||||
@@ -15,32 +16,32 @@ from dbt.events.types import (
|
||||
class AdapterLogger:
|
||||
name: str
|
||||
|
||||
def debug(self, msg, *args):
|
||||
def debug(self, msg, *args) -> None:
|
||||
event = AdapterEventDebug(
|
||||
name=self.name, base_msg=str(msg), args=list(args), node_info=get_node_info()
|
||||
)
|
||||
fire_event(event)
|
||||
|
||||
def info(self, msg, *args):
|
||||
def info(self, msg, *args) -> None:
|
||||
event = AdapterEventInfo(
|
||||
name=self.name, base_msg=str(msg), args=list(args), node_info=get_node_info()
|
||||
)
|
||||
fire_event(event)
|
||||
|
||||
def warning(self, msg, *args):
|
||||
def warning(self, msg, *args) -> None:
|
||||
event = AdapterEventWarning(
|
||||
name=self.name, base_msg=str(msg), args=list(args), node_info=get_node_info()
|
||||
)
|
||||
fire_event(event)
|
||||
|
||||
def error(self, msg, *args):
|
||||
def error(self, msg, *args) -> None:
|
||||
event = AdapterEventError(
|
||||
name=self.name, base_msg=str(msg), args=list(args), node_info=get_node_info()
|
||||
)
|
||||
fire_event(event)
|
||||
|
||||
# The default exc_info=True is what makes this method different
|
||||
def exception(self, msg, *args):
|
||||
def exception(self, msg, *args) -> None:
|
||||
exc_info = str(traceback.format_exc())
|
||||
event = AdapterEventError(
|
||||
name=self.name,
|
||||
@@ -51,8 +52,15 @@ class AdapterLogger:
|
||||
)
|
||||
fire_event(event)
|
||||
|
||||
def critical(self, msg, *args):
|
||||
def critical(self, msg, *args) -> None:
|
||||
event = AdapterEventError(
|
||||
name=self.name, base_msg=str(msg), args=list(args), node_info=get_node_info()
|
||||
)
|
||||
fire_event(event)
|
||||
|
||||
@staticmethod
|
||||
def set_adapter_dependency_log_level(package_name, level):
|
||||
"""By default, dbt suppresses non-dbt package logs. This method allows
|
||||
you to set the log level for a specific package.
|
||||
"""
|
||||
set_package_logging(package_name, level, EVENT_MANAGER)
|
||||
|
||||
@@ -37,7 +37,7 @@ def get_pid() -> int:
|
||||
return os.getpid()
|
||||
|
||||
|
||||
# in theory threads can change so we don't cache them.
|
||||
# in theory threads can change, so we don't cache them.
|
||||
def get_thread_name() -> str:
|
||||
return threading.current_thread().name
|
||||
|
||||
@@ -55,7 +55,7 @@ class EventLevel(str, Enum):
|
||||
class BaseEvent:
|
||||
"""BaseEvent for proto message generated python events"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
class_name = type(self).__name__
|
||||
msg_cls = getattr(types_pb2, class_name)
|
||||
if class_name == "Formatting" and len(args) > 0:
|
||||
@@ -100,9 +100,12 @@ class BaseEvent:
|
||||
self.pb_msg, preserving_proto_field_name=True, including_default_value_fields=True
|
||||
)
|
||||
|
||||
def to_json(self):
|
||||
def to_json(self) -> str:
|
||||
return MessageToJson(
|
||||
self.pb_msg, preserving_proto_field_name=True, including_default_valud_fields=True
|
||||
self.pb_msg,
|
||||
preserving_proto_field_name=True,
|
||||
including_default_value_fields=True,
|
||||
indent=None,
|
||||
)
|
||||
|
||||
def level_tag(self) -> EventLevel:
|
||||
|
||||
40
core/dbt/events/event_handler.py
Normal file
40
core/dbt/events/event_handler.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
from dbt.events.base_types import EventLevel
|
||||
from dbt.events.types import Note
|
||||
|
||||
from dbt.events.eventmgr import IEventManager
|
||||
|
||||
_log_level_to_event_level_map = {
|
||||
logging.DEBUG: EventLevel.DEBUG,
|
||||
logging.INFO: EventLevel.INFO,
|
||||
logging.WARN: EventLevel.WARN,
|
||||
logging.WARNING: EventLevel.WARN,
|
||||
logging.ERROR: EventLevel.ERROR,
|
||||
logging.CRITICAL: EventLevel.ERROR,
|
||||
}
|
||||
|
||||
|
||||
class DbtEventLoggingHandler(logging.Handler):
|
||||
"""A logging handler that wraps the EventManager
|
||||
This allows non-dbt packages to log to the dbt event stream.
|
||||
All logs are generated as "Note" events.
|
||||
"""
|
||||
|
||||
def __init__(self, event_manager: IEventManager, level):
|
||||
super().__init__(level)
|
||||
self.event_manager = event_manager
|
||||
|
||||
def emit(self, record: logging.LogRecord):
|
||||
note = Note(msg=record.getMessage())
|
||||
level = _log_level_to_event_level_map[record.levelno]
|
||||
self.event_manager.fire_event(e=note, level=level)
|
||||
|
||||
|
||||
def set_package_logging(package_name: str, log_level: Union[str, int], event_mgr: IEventManager):
|
||||
"""Attach dbt's custom logging handler to the package's logger."""
|
||||
log = logging.getLogger(package_name)
|
||||
log.setLevel(log_level)
|
||||
event_handler = DbtEventLoggingHandler(event_manager=event_mgr, level=log_level)
|
||||
log.addHandler(event_handler)
|
||||
@@ -1,183 +1,10 @@
|
||||
import os
|
||||
from colorama import Style
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
import json
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import threading
|
||||
import traceback
|
||||
from typing import Any, Callable, List, Optional, TextIO
|
||||
from typing import Callable, List, Optional, Protocol, Tuple
|
||||
from uuid import uuid4
|
||||
from dbt.events.format import timestamp_to_datetime_string
|
||||
|
||||
from dbt.events.base_types import BaseEvent, EventLevel, msg_from_base_event, EventMsg
|
||||
|
||||
|
||||
# A Filter is a function which takes a BaseEvent and returns True if the event
|
||||
# should be logged, False otherwise.
|
||||
Filter = Callable[[EventMsg], bool]
|
||||
|
||||
|
||||
# Default filter which logs every event
|
||||
def NoFilter(_: EventMsg) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# A Scrubber removes secrets from an input string, returning a sanitized string.
|
||||
Scrubber = Callable[[str], str]
|
||||
|
||||
|
||||
# Provide a pass-through scrubber implementation, also used as a default
|
||||
def NoScrubber(s: str) -> str:
|
||||
return s
|
||||
|
||||
|
||||
class LineFormat(Enum):
|
||||
PlainText = 1
|
||||
DebugText = 2
|
||||
Json = 3
|
||||
|
||||
|
||||
# Map from dbt event levels to python log levels
|
||||
_log_level_map = {
|
||||
EventLevel.DEBUG: 10,
|
||||
EventLevel.TEST: 10,
|
||||
EventLevel.INFO: 20,
|
||||
EventLevel.WARN: 30,
|
||||
EventLevel.ERROR: 40,
|
||||
}
|
||||
|
||||
|
||||
# We need this function for now because the numeric log severity levels in
|
||||
# Python do not match those for logbook, so we have to explicitly call the
|
||||
# correct function by name.
|
||||
def send_to_logger(l, level: str, log_line: str):
|
||||
if level == "test":
|
||||
l.debug(log_line)
|
||||
elif level == "debug":
|
||||
l.debug(log_line)
|
||||
elif level == "info":
|
||||
l.info(log_line)
|
||||
elif level == "warn":
|
||||
l.warning(log_line)
|
||||
elif level == "error":
|
||||
l.error(log_line)
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"While attempting to log {log_line}, encountered the unhandled level: {level}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoggerConfig:
|
||||
name: str
|
||||
filter: Filter = NoFilter
|
||||
scrubber: Scrubber = NoScrubber
|
||||
line_format: LineFormat = LineFormat.PlainText
|
||||
level: EventLevel = EventLevel.WARN
|
||||
use_colors: bool = False
|
||||
output_stream: Optional[TextIO] = None
|
||||
output_file_name: Optional[str] = None
|
||||
logger: Optional[Any] = None
|
||||
|
||||
|
||||
class _Logger:
|
||||
def __init__(self, event_manager: "EventManager", config: LoggerConfig) -> None:
|
||||
self.name: str = config.name
|
||||
self.filter: Filter = config.filter
|
||||
self.scrubber: Scrubber = config.scrubber
|
||||
self.level: EventLevel = config.level
|
||||
self.event_manager: EventManager = event_manager
|
||||
self._python_logger: Optional[logging.Logger] = config.logger
|
||||
|
||||
if config.output_stream is not None:
|
||||
stream_handler = logging.StreamHandler(config.output_stream)
|
||||
self._python_logger = self._get_python_log_for_handler(stream_handler)
|
||||
|
||||
if config.output_file_name:
|
||||
file_handler = RotatingFileHandler(
|
||||
filename=str(config.output_file_name),
|
||||
encoding="utf8",
|
||||
maxBytes=10 * 1024 * 1024, # 10 mb
|
||||
backupCount=5,
|
||||
)
|
||||
self._python_logger = self._get_python_log_for_handler(file_handler)
|
||||
|
||||
def _get_python_log_for_handler(self, handler: logging.Handler):
|
||||
log = logging.getLogger(self.name)
|
||||
log.setLevel(_log_level_map[self.level])
|
||||
handler.setFormatter(logging.Formatter(fmt="%(message)s"))
|
||||
log.handlers.clear()
|
||||
log.propagate = False
|
||||
log.addHandler(handler)
|
||||
return log
|
||||
|
||||
def create_line(self, msg: EventMsg) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
def write_line(self, msg: EventMsg):
|
||||
line = self.create_line(msg)
|
||||
if self._python_logger is not None:
|
||||
send_to_logger(self._python_logger, msg.info.level, line)
|
||||
|
||||
def flush(self):
|
||||
if self._python_logger is not None:
|
||||
for handler in self._python_logger.handlers:
|
||||
handler.flush()
|
||||
|
||||
|
||||
class _TextLogger(_Logger):
|
||||
def __init__(self, event_manager: "EventManager", config: LoggerConfig) -> None:
|
||||
super().__init__(event_manager, config)
|
||||
self.use_colors = config.use_colors
|
||||
self.use_debug_format = config.line_format == LineFormat.DebugText
|
||||
|
||||
def create_line(self, msg: EventMsg) -> str:
|
||||
return self.create_debug_line(msg) if self.use_debug_format else self.create_info_line(msg)
|
||||
|
||||
def create_info_line(self, msg: EventMsg) -> str:
|
||||
ts: str = datetime.utcnow().strftime("%H:%M:%S")
|
||||
scrubbed_msg: str = self.scrubber(msg.info.msg) # type: ignore
|
||||
return f"{self._get_color_tag()}{ts} {scrubbed_msg}"
|
||||
|
||||
def create_debug_line(self, msg: EventMsg) -> str:
|
||||
log_line: str = ""
|
||||
# Create a separator if this is the beginning of an invocation
|
||||
# TODO: This is an ugly hack, get rid of it if we can
|
||||
ts: str = timestamp_to_datetime_string(msg.info.ts)
|
||||
if msg.info.name == "MainReportVersion":
|
||||
separator = 30 * "="
|
||||
log_line = f"\n\n{separator} {ts} | {self.event_manager.invocation_id} {separator}\n"
|
||||
scrubbed_msg: str = self.scrubber(msg.info.msg) # type: ignore
|
||||
level = msg.info.level
|
||||
log_line += (
|
||||
f"{self._get_color_tag()}{ts} [{level:<5}]{self._get_thread_name()} {scrubbed_msg}"
|
||||
)
|
||||
return log_line
|
||||
|
||||
def _get_color_tag(self) -> str:
|
||||
return "" if not self.use_colors else Style.RESET_ALL
|
||||
|
||||
def _get_thread_name(self) -> str:
|
||||
thread_name = ""
|
||||
if threading.current_thread().name:
|
||||
thread_name = threading.current_thread().name
|
||||
thread_name = thread_name[:10]
|
||||
thread_name = thread_name.ljust(10, " ")
|
||||
thread_name = f" [{thread_name}]:"
|
||||
return thread_name
|
||||
|
||||
|
||||
class _JsonLogger(_Logger):
|
||||
def create_line(self, msg: EventMsg) -> str:
|
||||
from dbt.events.functions import msg_to_dict
|
||||
|
||||
msg_dict = msg_to_dict(msg)
|
||||
raw_log_line = json.dumps(msg_dict, sort_keys=True)
|
||||
line = self.scrubber(raw_log_line) # type: ignore
|
||||
return line
|
||||
from dbt.events.logger import LoggerConfig, _Logger, _TextLogger, _JsonLogger, LineFormat
|
||||
|
||||
|
||||
class EventManager:
|
||||
@@ -205,15 +32,36 @@ class EventManager:
|
||||
for callback in self.callbacks:
|
||||
callback(msg)
|
||||
|
||||
def add_logger(self, config: LoggerConfig):
|
||||
def add_logger(self, config: LoggerConfig) -> None:
|
||||
logger = (
|
||||
_JsonLogger(self, config)
|
||||
if config.line_format == LineFormat.Json
|
||||
else _TextLogger(self, config)
|
||||
_JsonLogger(config) if config.line_format == LineFormat.Json else _TextLogger(config)
|
||||
)
|
||||
logger.event_manager = self
|
||||
self.loggers.append(logger)
|
||||
|
||||
def flush(self):
|
||||
def flush(self) -> None:
|
||||
for logger in self.loggers:
|
||||
logger.flush()
|
||||
|
||||
|
||||
class IEventManager(Protocol):
|
||||
callbacks: List[Callable[[EventMsg], None]]
|
||||
invocation_id: str
|
||||
loggers: List[_Logger]
|
||||
|
||||
def fire_event(self, e: BaseEvent, level: Optional[EventLevel] = None) -> None:
|
||||
...
|
||||
|
||||
def add_logger(self, config: LoggerConfig) -> None:
|
||||
...
|
||||
|
||||
|
||||
class TestEventManager(IEventManager):
|
||||
def __init__(self) -> None:
|
||||
self.event_history: List[Tuple[BaseEvent, Optional[EventLevel]]] = []
|
||||
self.loggers = []
|
||||
|
||||
def fire_event(self, e: BaseEvent, level: Optional[EventLevel] = None) -> None:
|
||||
self.event_history.append((e, level))
|
||||
|
||||
def add_logger(self, config: LoggerConfig) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -44,13 +44,13 @@ def _pluralize(string: Union[str, NodeType]) -> str:
|
||||
return convert.pluralize()
|
||||
|
||||
|
||||
def pluralize(count, string: Union[str, NodeType]):
|
||||
def pluralize(count, string: Union[str, NodeType]) -> str:
|
||||
pluralized: str = str(string)
|
||||
if count != 1:
|
||||
pluralized = _pluralize(string)
|
||||
return f"{count} {pluralized}"
|
||||
|
||||
|
||||
def timestamp_to_datetime_string(ts):
|
||||
def timestamp_to_datetime_string(ts) -> str:
|
||||
timestamp_dt = datetime.fromtimestamp(ts.seconds + ts.nanos / 1e9)
|
||||
return timestamp_dt.strftime("%H:%M:%S.%f")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from dbt.constants import METADATA_ENV_PREFIX
|
||||
from dbt.events.base_types import BaseEvent, EventLevel, EventMsg
|
||||
from dbt.events.eventmgr import EventManager, LoggerConfig, LineFormat, NoFilter
|
||||
from dbt.events.helpers import env_secrets, scrub_secrets
|
||||
from dbt.events.types import Formatting, Note
|
||||
from dbt.events.eventmgr import EventManager, IEventManager
|
||||
from dbt.events.logger import LoggerConfig, NoFilter, LineFormat
|
||||
from dbt.exceptions import scrub_secrets, env_secrets
|
||||
from dbt.events.types import Note
|
||||
from dbt.flags import get_flags, ENABLE_LEGACY_LOGGER
|
||||
from dbt.logger import GLOBAL_LOGGER, make_log_dir_if_missing
|
||||
from functools import partial
|
||||
@@ -13,6 +14,7 @@ from typing import Callable, Dict, List, Optional, TextIO
|
||||
import uuid
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
|
||||
import dbt.utils
|
||||
|
||||
LOG_VERSION = 3
|
||||
metadata_vars: Optional[Dict[str, str]] = None
|
||||
@@ -67,7 +69,11 @@ def setup_event_logger(flags, callbacks: List[Callable[[EventMsg], None]] = [])
|
||||
log_level_file = EventLevel.DEBUG if flags.DEBUG else EventLevel(flags.LOG_LEVEL_FILE)
|
||||
EVENT_MANAGER.add_logger(
|
||||
_get_logfile_config(
|
||||
log_file, flags.USE_COLORS_FILE, log_file_format, log_level_file
|
||||
log_file,
|
||||
flags.USE_COLORS_FILE,
|
||||
log_file_format,
|
||||
log_level_file,
|
||||
flags.LOG_FILE_MAX_BYTES,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -89,7 +95,6 @@ def _get_stdout_config(
|
||||
level: EventLevel,
|
||||
log_cache_events: bool,
|
||||
) -> LoggerConfig:
|
||||
|
||||
return LoggerConfig(
|
||||
name="stdout_log",
|
||||
level=level,
|
||||
@@ -101,6 +106,7 @@ def _get_stdout_config(
|
||||
log_cache_events,
|
||||
line_format,
|
||||
),
|
||||
invocation_id=EVENT_MANAGER.invocation_id,
|
||||
output_stream=sys.stdout,
|
||||
)
|
||||
|
||||
@@ -110,14 +116,17 @@ def _stdout_filter(
|
||||
line_format: LineFormat,
|
||||
msg: EventMsg,
|
||||
) -> bool:
|
||||
return (msg.info.name not in ["CacheAction", "CacheDumpGraph"] or log_cache_events) and not (
|
||||
line_format == LineFormat.Json and type(msg.data) == Formatting
|
||||
)
|
||||
return msg.info.name not in ["CacheAction", "CacheDumpGraph"] or log_cache_events
|
||||
|
||||
|
||||
def _get_logfile_config(
|
||||
log_path: str, use_colors: bool, line_format: LineFormat, level: EventLevel
|
||||
log_path: str,
|
||||
use_colors: bool,
|
||||
line_format: LineFormat,
|
||||
level: EventLevel,
|
||||
log_file_max_bytes: int,
|
||||
) -> LoggerConfig:
|
||||
|
||||
return LoggerConfig(
|
||||
name="file_log",
|
||||
line_format=line_format,
|
||||
@@ -125,15 +134,15 @@ def _get_logfile_config(
|
||||
level=level, # File log is *always* debug level
|
||||
scrubber=env_scrubber,
|
||||
filter=partial(_logfile_filter, bool(get_flags().LOG_CACHE_EVENTS), line_format),
|
||||
invocation_id=EVENT_MANAGER.invocation_id,
|
||||
output_file_name=log_path,
|
||||
output_file_max_bytes=log_file_max_bytes,
|
||||
)
|
||||
|
||||
|
||||
def _logfile_filter(log_cache_events: bool, line_format: LineFormat, msg: EventMsg) -> bool:
|
||||
return (
|
||||
msg.info.code not in nofile_codes
|
||||
and not (msg.info.name in ["CacheAction", "CacheDumpGraph"] and not log_cache_events)
|
||||
and not (line_format == LineFormat.Json and type(msg.data) == Formatting)
|
||||
return msg.info.code not in nofile_codes and not (
|
||||
msg.info.name in ["CacheAction", "CacheDumpGraph"] and not log_cache_events
|
||||
)
|
||||
|
||||
|
||||
@@ -161,7 +170,7 @@ def env_scrubber(msg: str) -> str:
|
||||
return scrub_secrets(msg, env_secrets())
|
||||
|
||||
|
||||
def cleanup_event_logger():
|
||||
def cleanup_event_logger() -> None:
|
||||
# Reset to a no-op manager to release streams associated with logs. This is
|
||||
# especially important for tests, since pytest replaces the stdout stream
|
||||
# during test runs, and closes the stream after the test is over.
|
||||
@@ -172,7 +181,7 @@ def cleanup_event_logger():
|
||||
# Since dbt-rpc does not do its own log setup, and since some events can
|
||||
# currently fire before logs can be configured by setup_event_logger(), we
|
||||
# create a default configuration with default settings and no file output.
|
||||
EVENT_MANAGER: EventManager = EventManager()
|
||||
EVENT_MANAGER: IEventManager = EventManager()
|
||||
EVENT_MANAGER.add_logger(
|
||||
_get_logbook_log_config(False, True, False, False) # type: ignore
|
||||
if ENABLE_LEGACY_LOGGER
|
||||
@@ -186,12 +195,12 @@ _CAPTURE_STREAM: Optional[TextIO] = None
|
||||
|
||||
|
||||
# used for integration tests
|
||||
def capture_stdout_logs(stream: TextIO):
|
||||
def capture_stdout_logs(stream: TextIO) -> None:
|
||||
global _CAPTURE_STREAM
|
||||
_CAPTURE_STREAM = stream
|
||||
|
||||
|
||||
def stop_capture_stdout_logs():
|
||||
def stop_capture_stdout_logs() -> None:
|
||||
global _CAPTURE_STREAM
|
||||
_CAPTURE_STREAM = None
|
||||
|
||||
@@ -200,7 +209,7 @@ def stop_capture_stdout_logs():
|
||||
# the message may contain secrets which must be scrubbed at the usage site.
|
||||
def msg_to_json(msg: EventMsg) -> str:
|
||||
msg_dict = msg_to_dict(msg)
|
||||
raw_log_line = json.dumps(msg_dict, sort_keys=True)
|
||||
raw_log_line = json.dumps(msg_dict, sort_keys=True, cls=dbt.utils.ForgivingJSONEncoder)
|
||||
return raw_log_line
|
||||
|
||||
|
||||
@@ -225,7 +234,7 @@ def msg_to_dict(msg: EventMsg) -> dict:
|
||||
return msg_dict
|
||||
|
||||
|
||||
def warn_or_error(event, node=None):
|
||||
def warn_or_error(event, node=None) -> None:
|
||||
flags = get_flags()
|
||||
if flags.WARN_ERROR or flags.WARN_ERROR_OPTIONS.includes(type(event).__name__):
|
||||
|
||||
@@ -263,7 +272,7 @@ def fire_event(e: BaseEvent, level: Optional[EventLevel] = None) -> None:
|
||||
|
||||
def get_metadata_vars() -> Dict[str, str]:
|
||||
global metadata_vars
|
||||
if metadata_vars is None:
|
||||
if not metadata_vars:
|
||||
metadata_vars = {
|
||||
k[len(METADATA_ENV_PREFIX) :]: v
|
||||
for k, v in os.environ.items()
|
||||
@@ -285,3 +294,8 @@ def set_invocation_id() -> None:
|
||||
# This is primarily for setting the invocation_id for separate
|
||||
# commands in the dbt servers. It shouldn't be necessary for the CLI.
|
||||
EVENT_MANAGER.invocation_id = str(uuid.uuid4())
|
||||
|
||||
|
||||
def ctx_set_event_manager(event_manager: IEventManager) -> None:
|
||||
global EVENT_MANAGER
|
||||
EVENT_MANAGER = event_manager
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
import os
|
||||
from typing import List
|
||||
from dbt.constants import SECRET_ENV_PREFIX
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def env_secrets() -> List[str]:
|
||||
return [v for k, v in os.environ.items() if k.startswith(SECRET_ENV_PREFIX) and v.strip()]
|
||||
|
||||
|
||||
def scrub_secrets(msg: str, secrets: List[str]) -> str:
|
||||
scrubbed = str(msg)
|
||||
|
||||
for secret in secrets:
|
||||
scrubbed = scrubbed.replace(secret, "*****")
|
||||
|
||||
return scrubbed
|
||||
|
||||
|
||||
# This converts a datetime to a json format datetime string which
|
||||
# is used in constructing protobuf message timestamps.
|
||||
def datetime_to_json_string(dt: datetime) -> str:
|
||||
|
||||
180
core/dbt/events/logger.py
Normal file
180
core/dbt/events/logger.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Optional, TextIO, Any, Callable
|
||||
|
||||
from colorama import Style
|
||||
|
||||
import dbt.utils
|
||||
from dbt.events.base_types import EventLevel, EventMsg
|
||||
from dbt.events.format import timestamp_to_datetime_string
|
||||
|
||||
# A Filter is a function which takes a BaseEvent and returns True if the event
|
||||
# should be logged, False otherwise.
|
||||
Filter = Callable[[EventMsg], bool]
|
||||
|
||||
|
||||
# Default filter which logs every event
|
||||
def NoFilter(_: EventMsg) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# A Scrubber removes secrets from an input string, returning a sanitized string.
|
||||
Scrubber = Callable[[str], str]
|
||||
|
||||
|
||||
# Provide a pass-through scrubber implementation, also used as a default
|
||||
def NoScrubber(s: str) -> str:
|
||||
return s
|
||||
|
||||
|
||||
class LineFormat(Enum):
|
||||
PlainText = 1
|
||||
DebugText = 2
|
||||
Json = 3
|
||||
|
||||
|
||||
# Map from dbt event levels to python log levels
|
||||
_log_level_map = {
|
||||
EventLevel.DEBUG: 10,
|
||||
EventLevel.TEST: 10,
|
||||
EventLevel.INFO: 20,
|
||||
EventLevel.WARN: 30,
|
||||
EventLevel.ERROR: 40,
|
||||
}
|
||||
|
||||
|
||||
# We need this function for now because the numeric log severity levels in
|
||||
# Python do not match those for logbook, so we have to explicitly call the
|
||||
# correct function by name.
|
||||
def send_to_logger(l, level: str, log_line: str):
|
||||
if level == "test":
|
||||
l.debug(log_line)
|
||||
elif level == "debug":
|
||||
l.debug(log_line)
|
||||
elif level == "info":
|
||||
l.info(log_line)
|
||||
elif level == "warn":
|
||||
l.warning(log_line)
|
||||
elif level == "error":
|
||||
l.error(log_line)
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"While attempting to log {log_line}, encountered the unhandled level: {level}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoggerConfig:
|
||||
name: str
|
||||
filter: Filter = NoFilter
|
||||
scrubber: Scrubber = NoScrubber
|
||||
line_format: LineFormat = LineFormat.PlainText
|
||||
level: EventLevel = EventLevel.WARN
|
||||
invocation_id: Optional[str] = None
|
||||
use_colors: bool = False
|
||||
output_stream: Optional[TextIO] = None
|
||||
output_file_name: Optional[str] = None
|
||||
output_file_max_bytes: Optional[int] = 10 * 1024 * 1024 # 10 mb
|
||||
logger: Optional[Any] = None
|
||||
|
||||
|
||||
class _Logger:
|
||||
def __init__(self, config: LoggerConfig) -> None:
|
||||
self.name: str = config.name
|
||||
self.filter: Filter = config.filter
|
||||
self.scrubber: Scrubber = config.scrubber
|
||||
self.level: EventLevel = config.level
|
||||
self.invocation_id: Optional[str] = config.invocation_id
|
||||
self._python_logger: Optional[logging.Logger] = config.logger
|
||||
|
||||
if config.output_stream is not None:
|
||||
stream_handler = logging.StreamHandler(config.output_stream)
|
||||
self._python_logger = self._get_python_log_for_handler(stream_handler)
|
||||
|
||||
if config.output_file_name:
|
||||
file_handler = RotatingFileHandler(
|
||||
filename=str(config.output_file_name),
|
||||
encoding="utf8",
|
||||
maxBytes=config.output_file_max_bytes, # type: ignore
|
||||
backupCount=5,
|
||||
)
|
||||
self._python_logger = self._get_python_log_for_handler(file_handler)
|
||||
|
||||
def _get_python_log_for_handler(self, handler: logging.Handler):
|
||||
log = logging.getLogger(self.name)
|
||||
log.setLevel(_log_level_map[self.level])
|
||||
handler.setFormatter(logging.Formatter(fmt="%(message)s"))
|
||||
log.handlers.clear()
|
||||
log.propagate = False
|
||||
log.addHandler(handler)
|
||||
return log
|
||||
|
||||
def create_line(self, msg: EventMsg) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
def write_line(self, msg: EventMsg):
|
||||
line = self.create_line(msg)
|
||||
if self._python_logger is not None:
|
||||
send_to_logger(self._python_logger, msg.info.level, line)
|
||||
|
||||
def flush(self):
|
||||
if self._python_logger is not None:
|
||||
for handler in self._python_logger.handlers:
|
||||
handler.flush()
|
||||
|
||||
|
||||
class _TextLogger(_Logger):
|
||||
def __init__(self, config: LoggerConfig) -> None:
|
||||
super().__init__(config)
|
||||
self.use_colors = config.use_colors
|
||||
self.use_debug_format = config.line_format == LineFormat.DebugText
|
||||
|
||||
def create_line(self, msg: EventMsg) -> str:
|
||||
return self.create_debug_line(msg) if self.use_debug_format else self.create_info_line(msg)
|
||||
|
||||
def create_info_line(self, msg: EventMsg) -> str:
|
||||
ts: str = datetime.utcnow().strftime("%H:%M:%S")
|
||||
scrubbed_msg: str = self.scrubber(msg.info.msg) # type: ignore
|
||||
return f"{self._get_color_tag()}{ts} {scrubbed_msg}"
|
||||
|
||||
def create_debug_line(self, msg: EventMsg) -> str:
|
||||
log_line: str = ""
|
||||
# Create a separator if this is the beginning of an invocation
|
||||
# TODO: This is an ugly hack, get rid of it if we can
|
||||
ts: str = timestamp_to_datetime_string(msg.info.ts)
|
||||
if msg.info.name == "MainReportVersion":
|
||||
separator = 30 * "="
|
||||
log_line = f"\n\n{separator} {ts} | {self.invocation_id} {separator}\n"
|
||||
scrubbed_msg: str = self.scrubber(msg.info.msg) # type: ignore
|
||||
level = msg.info.level
|
||||
log_line += (
|
||||
f"{self._get_color_tag()}{ts} [{level:<5}]{self._get_thread_name()} {scrubbed_msg}"
|
||||
)
|
||||
return log_line
|
||||
|
||||
def _get_color_tag(self) -> str:
|
||||
return "" if not self.use_colors else Style.RESET_ALL
|
||||
|
||||
def _get_thread_name(self) -> str:
|
||||
thread_name = ""
|
||||
if threading.current_thread().name:
|
||||
thread_name = threading.current_thread().name
|
||||
thread_name = thread_name[:10]
|
||||
thread_name = thread_name.ljust(10, " ")
|
||||
thread_name = f" [{thread_name}]:"
|
||||
return thread_name
|
||||
|
||||
|
||||
class _JsonLogger(_Logger):
|
||||
def create_line(self, msg: EventMsg) -> str:
|
||||
from dbt.events.functions import msg_to_dict
|
||||
|
||||
msg_dict = msg_to_dict(msg)
|
||||
raw_log_line = json.dumps(msg_dict, sort_keys=True, cls=dbt.utils.ForgivingJSONEncoder)
|
||||
line = self.scrubber(raw_log_line) # type: ignore
|
||||
return line
|
||||
@@ -66,6 +66,27 @@ message ReferenceKeyMsg {
|
||||
string identifier = 3;
|
||||
}
|
||||
|
||||
//ColumnType
|
||||
message ColumnType {
|
||||
string column_name = 1;
|
||||
string previous_column_type = 2;
|
||||
string current_column_type = 3;
|
||||
}
|
||||
|
||||
// ColumnConstraint
|
||||
message ColumnConstraint {
|
||||
string column_name = 1;
|
||||
string constraint_name = 2;
|
||||
string constraint_type = 3;
|
||||
}
|
||||
|
||||
// ModelConstraint
|
||||
message ModelConstraint {
|
||||
string constraint_name = 1;
|
||||
string constraint_type = 2;
|
||||
repeated string columns = 3;
|
||||
}
|
||||
|
||||
// GenericMessage, used for deserializing only
|
||||
message GenericMessage {
|
||||
EventInfo info = 1;
|
||||
@@ -1248,6 +1269,46 @@ message SemanticValidationFailureMsg {
|
||||
SemanticValidationFailure data = 2;
|
||||
}
|
||||
|
||||
// I071
|
||||
message UnversionedBreakingChange {
|
||||
repeated string breaking_changes = 1;
|
||||
string model_name = 2;
|
||||
string model_file_path = 3;
|
||||
bool contract_enforced_disabled = 4;
|
||||
repeated string columns_removed = 5;
|
||||
repeated ColumnType column_type_changes = 6;
|
||||
repeated ColumnConstraint enforced_column_constraint_removed = 7;
|
||||
repeated ModelConstraint enforced_model_constraint_removed = 8;
|
||||
repeated string materialization_changed = 9;
|
||||
}
|
||||
|
||||
message UnversionedBreakingChangeMsg {
|
||||
EventInfo info = 1;
|
||||
UnversionedBreakingChange data = 2;
|
||||
}
|
||||
|
||||
// I072
|
||||
message WarnStateTargetEqual {
|
||||
string state_path = 1;
|
||||
}
|
||||
|
||||
message WarnStateTargetEqualMsg {
|
||||
EventInfo info = 1;
|
||||
WarnStateTargetEqual data = 2;
|
||||
}
|
||||
|
||||
// I073
|
||||
message FreshnessConfigProblem {
|
||||
string msg = 1;
|
||||
}
|
||||
|
||||
message FreshnessConfigProblemMsg {
|
||||
EventInfo info = 1;
|
||||
FreshnessConfigProblem data = 2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// M - Deps generation
|
||||
|
||||
@@ -1538,6 +1599,58 @@ message NoNodesForSelectionCriteriaMsg {
|
||||
NoNodesForSelectionCriteria data = 2;
|
||||
}
|
||||
|
||||
// M031
|
||||
message DepsLockUpdating{
|
||||
string lock_filepath = 1;
|
||||
}
|
||||
|
||||
message DepsLockUpdatingMsg{
|
||||
EventInfo info = 1;
|
||||
DepsLockUpdating data = 2;
|
||||
}
|
||||
|
||||
// M032
|
||||
message DepsAddPackage{
|
||||
string package_name = 1;
|
||||
string version = 2;
|
||||
string packages_filepath = 3;
|
||||
}
|
||||
|
||||
message DepsAddPackageMsg{
|
||||
EventInfo info = 1;
|
||||
DepsAddPackage data = 2;
|
||||
}
|
||||
|
||||
//M033
|
||||
message DepsFoundDuplicatePackage{
|
||||
map<string, string> removed_package = 1;
|
||||
}
|
||||
|
||||
message DepsFoundDuplicatePackageMsg{
|
||||
EventInfo info = 1;
|
||||
DepsFoundDuplicatePackage data = 2;
|
||||
}
|
||||
|
||||
//M034
|
||||
message DepsVersionMissing{
|
||||
string source = 1;
|
||||
}
|
||||
|
||||
message DepsVersionMissingMsg{
|
||||
EventInfo info = 1;
|
||||
DepsVersionMissing data = 2;
|
||||
}
|
||||
|
||||
//M035
|
||||
message DepsScrubbedPackageName{
|
||||
string package_name = 1;
|
||||
}
|
||||
|
||||
message DepsScrubbedPackageNameMsg{
|
||||
EventInfo info = 1;
|
||||
DepsScrubbedPackageName data = 2;
|
||||
}
|
||||
|
||||
// Q - Node execution
|
||||
|
||||
// Q001
|
||||
@@ -1584,6 +1697,7 @@ message SeedHeaderMsg {
|
||||
message SQLRunnerException {
|
||||
string exc = 1;
|
||||
string exc_info = 2;
|
||||
NodeInfo node_info = 3;
|
||||
}
|
||||
|
||||
message SQLRunnerExceptionMsg {
|
||||
@@ -1650,6 +1764,7 @@ message LogSnapshotResult {
|
||||
int32 total = 5;
|
||||
float execution_time = 6;
|
||||
map<string, string> cfg = 7;
|
||||
string result_message = 8;
|
||||
}
|
||||
|
||||
message LogSnapshotResultMsg {
|
||||
@@ -1934,6 +2049,7 @@ message CatchableExceptionOnRunMsg {
|
||||
message InternalErrorOnRun {
|
||||
string build_path = 1;
|
||||
string exc = 2;
|
||||
NodeInfo node_info = 3;
|
||||
}
|
||||
|
||||
message InternalErrorOnRunMsg {
|
||||
@@ -1946,6 +2062,7 @@ message GenericExceptionOnRun {
|
||||
string build_path = 1;
|
||||
string unique_id = 2;
|
||||
string exc = 3;
|
||||
NodeInfo node_info = 4;
|
||||
}
|
||||
|
||||
message GenericExceptionOnRunMsg {
|
||||
@@ -2245,25 +2362,7 @@ message CheckNodeTestFailureMsg {
|
||||
CheckNodeTestFailure data = 2;
|
||||
}
|
||||
|
||||
// Z028
|
||||
message FirstRunResultError {
|
||||
string msg = 1;
|
||||
}
|
||||
|
||||
message FirstRunResultErrorMsg {
|
||||
EventInfo info = 1;
|
||||
FirstRunResultError data = 2;
|
||||
}
|
||||
|
||||
// Z029
|
||||
message AfterFirstRunResultError {
|
||||
string msg = 1;
|
||||
}
|
||||
|
||||
message AfterFirstRunResultErrorMsg {
|
||||
EventInfo info = 1;
|
||||
AfterFirstRunResultError data = 2;
|
||||
}
|
||||
// Skipped Z028, Z029
|
||||
|
||||
// Z030
|
||||
message EndOfRunSummary {
|
||||
@@ -2426,3 +2525,24 @@ message NoteMsg {
|
||||
EventInfo info = 1;
|
||||
Note data = 2;
|
||||
}
|
||||
|
||||
// Z051
|
||||
message ResourceReport {
|
||||
string command_name = 2;
|
||||
bool command_success = 3;
|
||||
float command_wall_clock_time = 4;
|
||||
// The process_* metrics reflect the resource consumption of the process as
|
||||
// a whole when the command completes. When dbt is being used as a library,
|
||||
// these will reflect the resource consumption of the host process as a whole,
|
||||
// rather than the resources used exclusively by the command.
|
||||
float process_user_time = 5;
|
||||
float process_kernel_time = 6;
|
||||
int64 process_mem_max_rss = 7;
|
||||
int64 process_in_blocks = 8;
|
||||
int64 process_out_blocks = 9;
|
||||
}
|
||||
|
||||
message ResourceReportMsg {
|
||||
EventInfo info = 1;
|
||||
ResourceReport data = 2;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user