From 8dcf1c93770943301b8210d09d1b3f833f816de9 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 12:09:09 +0100 Subject: [PATCH 001/157] Initial commit --- LICENSE.md | 202 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 47 +++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 LICENSE.md create mode 100644 README.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..427417b60 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 000000000..7093b140e --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +**CURRENTLY IN PRE-RELEASE, STAY TUNED FOR AN OFFICAL ANNOUNCEMENT AND FULL DOCUMENTATION** + +# dbtvault by [Data-Vault](www.data-vault.co.uk) + +dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses. + + +## Currently supported databases: + +- [SnowFlake](https://www.snowflake.com/about/) + +## Installation + +Add the following to your ```packages.yml``` + +```bash +packages: + + - git: "https://github.com/Datavault-UK/dbtvault" +``` +And run +```dbt deps``` + +[Read more on package installation](https://docs.getdbt.com/docs/package-management) + +## Usage + +1. Create a model for your hub, link or satellite +2. Set your metadata and hash model parameters +4. Call the appropriate template macro +```bash +{{- config(...) -}} + +{%- set metadata = ... -%} + +{%- set hash_model = ... -%} + +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} +``` + +## Contributing +Please open an issue first to discuss what you would like to change. + +## License +[Apache 2.0](https://choosealicense.com/licenses/apache-2.0/) From 9dcc33761bcd859ea35ffc96a643c19a81842b2a Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 12:11:29 +0100 Subject: [PATCH 002/157] Added all macros and initial files --- dbt_project.yml | 15 +++++++++ macros/hub_template.sql | 20 ++++++++++++ macros/internal/add_columns.sql | 8 +++++ macros/internal/create_col.sql | 5 +++ macros/internal/create_source.sql | 14 ++++++++ macros/internal/gen_hashing.sql | 10 ++++++ macros/internal/single.sql | 6 ++++ macros/internal/staging_footer.sql | 5 +++ macros/internal/union.sql | 22 +++++++++++++ macros/link_template.sql | 20 ++++++++++++ macros/sat_template.sql | 31 ++++++++++++++++++ macros/utility/cast.sql | 51 ++++++++++++++++++++++++++++++ macros/utility/md5_binary.sql | 23 ++++++++++++++ macros/utility/prefix.sql | 15 +++++++++ 14 files changed, 245 insertions(+) create mode 100644 dbt_project.yml create mode 100644 macros/hub_template.sql create mode 100644 macros/internal/add_columns.sql create mode 100644 macros/internal/create_col.sql create mode 100644 macros/internal/create_source.sql create mode 100644 macros/internal/gen_hashing.sql create mode 100644 macros/internal/single.sql create mode 100644 macros/internal/staging_footer.sql create mode 100644 macros/internal/union.sql create mode 100644 macros/link_template.sql create mode 100644 macros/sat_template.sql create mode 100644 macros/utility/cast.sql create mode 100644 macros/utility/md5_binary.sql create mode 100644 macros/utility/prefix.sql diff --git a/dbt_project.yml b/dbt_project.yml new file mode 100644 index 000000000..9c130b001 --- /dev/null +++ b/dbt_project.yml @@ -0,0 +1,15 @@ +name: 'dbtvault' +version: '1.0' + +profile: 'dbtvault' + +source-paths: ["models"] +analysis-paths: ["analysis"] +test-paths: ["tests"] +data-paths: ["data"] +macro-paths: ["macros"] + +target-path: "target" +clean-targets: + - "target" + - "dbt_modules" diff --git a/macros/hub_template.sql b/macros/hub_template.sql new file mode 100644 index 000000000..526f8a84f --- /dev/null +++ b/macros/hub_template.sql @@ -0,0 +1,20 @@ +{%- macro hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) -%} + +{%- set is_union = true if source|length > 1 else false -%} + +SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_nk, tgt_ldts, tgt_source], 'stg') }} +FROM ( + {{ dbtvault.create_source(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk|last, + source, is_union) }} +) AS stg +{% if is_incremental() or is_union -%} +LEFT JOIN {{ this }} AS tgt +ON {{ dbtvault.prefix([tgt_pk|last], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} +WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL +{%- if is_union %} +AND stg.FIRST_SOURCE IS NULL +{%- endif -%} +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/add_columns.sql b/macros/internal/add_columns.sql new file mode 100644 index 000000000..6f32e2972 --- /dev/null +++ b/macros/internal/add_columns.sql @@ -0,0 +1,8 @@ +{%- macro add_columns(pairs) -%} +{% for pair in pairs -%} + + {{ dbtvault.create_col(pair[0], pair[1]) }} + {%- if not loop.last -%} , {% endif %} +{% endfor %} + +{%- endmacro -%} diff --git a/macros/internal/create_col.sql b/macros/internal/create_col.sql new file mode 100644 index 000000000..cd61f25d4 --- /dev/null +++ b/macros/internal/create_col.sql @@ -0,0 +1,5 @@ +{%- macro create_col(column, alias) -%} + +{{ column }} AS {{ alias }} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/create_source.sql b/macros/internal/create_source.sql new file mode 100644 index 000000000..00131ca69 --- /dev/null +++ b/macros/internal/create_source.sql @@ -0,0 +1,14 @@ +{%- macro create_source(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk, + source, is_union) -%} + + {%- if not is_union -%} + + {{- dbtvault.single(src_pk, src_nk, src_ldts, src_source, tgt_pk, source[0], 'a') -}} + + {%- else -%} + + {{- dbtvault.union(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk, source) -}} + + {%- endif -%} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/gen_hashing.sql b/macros/internal/gen_hashing.sql new file mode 100644 index 000000000..f12867302 --- /dev/null +++ b/macros/internal/gen_hashing.sql @@ -0,0 +1,10 @@ +{%- macro gen_hashing(pairs) -%} + +SELECT +{% for pair in pairs -%} + + {{ dbtvault.md5_binary(pair[0], pair[1]) }} + {%- if not loop.last -%} , {% endif %} +{% endfor %} + +{%- endmacro -%} diff --git a/macros/internal/single.sql b/macros/internal/single.sql new file mode 100644 index 000000000..3a3b78058 --- /dev/null +++ b/macros/internal/single.sql @@ -0,0 +1,6 @@ +{%- macro single(src_pk, src_nk, src_ldts, src_source, tgt_pk, + source, letter='a') -%} + + SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], letter) }} + FROM {{ source }} AS {{ letter }} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/staging_footer.sql b/macros/internal/staging_footer.sql new file mode 100644 index 000000000..953594b65 --- /dev/null +++ b/macros/internal/staging_footer.sql @@ -0,0 +1,5 @@ +{%- macro staging_footer(loaddate, source, source_table) -%} +{%- if source or loaddate -%}, {%- endif -%} +{%- if loaddate -%} {{ loaddate }} AS LOADDATE, {%- endif -%} {%- if source -%} '{{ source }}' AS SOURCE {%- endif %} FROM {{ source_table }} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/union.sql b/macros/internal/union.sql new file mode 100644 index 000000000..75feac860 --- /dev/null +++ b/macros/internal/union.sql @@ -0,0 +1,22 @@ +{%- macro union(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk, source) -%} + + SELECT {{ tgt_cols|join(", ") }}{% if is_incremental() or union -%}, + LAG({{ src_source }}, 1) + OVER(PARTITION by {{ tgt_pk }} + ORDER BY {{ tgt_pk }}) AS FIRST_SOURCE + {%- endif %} + FROM ( + + {%- set letters='abcdefghijklmnopqrstuvwxyz' -%} + + {%- set iterations = source|length -%} + + {%- for src in range(iterations) -%} + {%- set letter = letters[loop.index0] %} + {{ dbtvault.single(src_pk[loop.index0], src_nk[loop.index0], src_ldts, src_source, + tgt_pk, source[loop.index0], letter) -}} + {% if not loop.last %} + UNION + {%- endif -%} + {%- endfor -%}) +{%- endmacro -%} \ No newline at end of file diff --git a/macros/link_template.sql b/macros/link_template.sql new file mode 100644 index 000000000..7882baab7 --- /dev/null +++ b/macros/link_template.sql @@ -0,0 +1,20 @@ +{%- macro link_template(src_pk, src_fk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) -%} + +{%- set is_union = true if source|length > 1 else false -%} + +SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_ldts, tgt_source], 'stg') }} +FROM ( + {{ dbtvault.create_source(src_pk, src_fk, src_ldts, src_source, tgt_cols, tgt_pk|last, + source, is_union) }} +) AS stg +{% if is_incremental() or is_union -%} +LEFT JOIN {{ this }} AS tgt +ON {{ dbtvault.prefix([tgt_pk|last], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} +WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL +{%- if is_union %} +AND stg.FIRST_SOURCE IS NULL +{%- endif -%} +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/sat_template.sql b/macros/sat_template.sql new file mode 100644 index 000000000..de907847e --- /dev/null +++ b/macros/sat_template.sql @@ -0,0 +1,31 @@ +{%- macro sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + tgt_cols, + tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source, + src_table, source) -%} + +SELECT DISTINCT {{ dbtvault.cast([tgt_hashdiff, tgt_pk, tgt_payload, tgt_ldts, tgt_eff, tgt_source], 'e') }} +FROM {{ source[0] }} AS e +{% if is_incremental() -%} +LEFT JOIN ( + SELECT {{ dbtvault.prefix(tgt_cols, 'd') }} + FROM ( + SELECT {{ dbtvault.prefix(tgt_cols, 'c') }}, + CASE WHEN RANK() + OVER (PARTITION BY {{ dbtvault.prefix([tgt_pk|last], 'c') }} + ORDER BY {{ dbtvault.prefix([tgt_ldts|last], 'c') }} DESC) = 1 + THEN 'Y' ELSE 'N' END CURR_FLG + FROM ( + SELECT {{ dbtvault.prefix(tgt_cols, 'a') }} + FROM {{ this }} as a + JOIN {{ source[0] }} as b + ON {{ dbtvault.prefix([tgt_pk|last], 'a') }} = {{ dbtvault.prefix([src_pk], 'b') }} + ) as c + ) AS d +WHERE d.CURR_FLG = 'Y') AS src +ON {{ dbtvault.prefix([tgt_hashdiff|last], 'src') }} = {{ dbtvault.prefix([src_hashdiff], 'e') }} +WHERE {{ dbtvault.prefix([tgt_hashdiff|last], 'src') }} IS NULL +{%- endif -%} + +{% endmacro %} \ No newline at end of file diff --git a/macros/utility/cast.sql b/macros/utility/cast.sql new file mode 100644 index 000000000..6826c5147 --- /dev/null +++ b/macros/utility/cast.sql @@ -0,0 +1,51 @@ +{%- macro cast(columns, prefix=none) -%} + +{#- If a string or list -#} +{%- if columns is iterable -%} + + {#- If only single string provided -#} + {%- if columns is string -%} + + {{columns}} + + {%- else -%} + + {%- for column in columns -%} + + {#- Output String if just a string -#} + {%- if column is string -%} + {%- if prefix -%} + {{ dbtvault.prefix([column], prefix) }} + {%- else -%} + {{column}} + {%- endif -%} + + {#- Recurse if a list of lists (i.e. multi-column key) -#} + {%- elif column|first is iterable and column|first is not string -%} + {{ dbtvault.cast(column, prefix) }} + + {#- Otherwise it is a standard list -#} + {%- else -%} + + {#- Make sure it is a triple -#} + {%- if column|length == 3 %} + {% if prefix -%} + CAST({{ dbtvault.prefix([column[0]], prefix) }} AS {{ column[1] }}) AS {{ column[2] }} + {%- else -%} + CAST({{ column[0] }} AS {{ column[1] }}) AS {{ column[2] }} + {%- endif -%} + {%- endif -%} + + {%- endif -%} + + {#- Add trailing comma if not last -#} + {%- if not loop.last -%} , {%- endif -%} + + {%- endfor -%} + + {%- endif -%} + +{%- endif -%} + +{%- endmacro -%} + diff --git a/macros/utility/md5_binary.sql b/macros/utility/md5_binary.sql new file mode 100644 index 000000000..0c8382957 --- /dev/null +++ b/macros/utility/md5_binary.sql @@ -0,0 +1,23 @@ +{%- macro md5_binary(columns, alias) -%} + +{%- if columns is string -%} + +CAST(MD5_BINARY(UPPER(TRIM(CAST({{columns}} AS VARCHAR)))) AS BINARY(16)) AS {{alias}} + +{%- else -%} + +CAST(MD5_BINARY(CONCAT( + +{%- for column in columns[:-1] -%} + +IFNULL(UPPER(TRIM(CAST({{column}} AS VARCHAR))), '^^'), '||', + +{%- if loop.last -%} + +IFNULL(UPPER(TRIM(CAST({{columns[-1]}} AS VARCHAR))), '^^') )) AS BINARY(16)) AS {{alias}} + +{%- endif -%} +{%- endfor -%} +{%- endif -%} +{%- endmacro -%} + diff --git a/macros/utility/prefix.sql b/macros/utility/prefix.sql new file mode 100644 index 000000000..b6643011e --- /dev/null +++ b/macros/utility/prefix.sql @@ -0,0 +1,15 @@ +{%- macro prefix(columns, prefix_str) -%} + +{%- for column in columns -%} + + {% if column is iterable and column is not string %} + {{- dbtvault.prefix(column, prefix_str) -}} + {%- else -%} + {{- prefix_str}}.{{column.strip() -}} + {%- endif -%} + + {%- if not loop.last -%} , {% endif %} + +{%- endfor -%} + +{%- endmacro -%} \ No newline at end of file From b2d30802c08d5538f6404a5a919435e4fe7d4a1e Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 13:10:28 +0100 Subject: [PATCH 003/157] Added initial RDT docs --- .readthedocs.yml | 12 ++++++++++++ docs/gettingstarted.md | 13 +++++++++++++ docs/images/docs-banner.png | Bin 0 -> 13950 bytes docs/images/favicon.ico | Bin 0 -> 1150 bytes docs/images/logo.png | Bin 0 -> 6512 bytes docs/index.md | 2 ++ docs/requirements.txt | 2 ++ mkdocs.yml | 29 +++++++++++++++++++++++++++++ 8 files changed, 58 insertions(+) create mode 100644 .readthedocs.yml create mode 100644 docs/gettingstarted.md create mode 100755 docs/images/docs-banner.png create mode 100755 docs/images/favicon.ico create mode 100755 docs/images/logo.png create mode 100644 docs/index.md create mode 100644 docs/requirements.txt create mode 100644 mkdocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..886638aa5 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,12 @@ +version: 2 + +# Build documentation with MkDocs +mkdocs: + configuration: mkdocs.yml + +formats: all + +python: + version: 3.7 + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md new file mode 100644 index 000000000..3143aa6f4 --- /dev/null +++ b/docs/gettingstarted.md @@ -0,0 +1,13 @@ +# Installation + +Add the following to your ```packages.yml```: + +```yaml +packages: + + - git: "https://github.com/Datavault-UK/dbtvault" +``` +And run +```dbt deps``` + +[Read more on package installation (from dbt)](https://docs.getdbt.com/docs/package-management) \ No newline at end of file diff --git a/docs/images/docs-banner.png b/docs/images/docs-banner.png new file mode 100755 index 0000000000000000000000000000000000000000..e47fea8637c3548a42e6c89a18b58f99cc849cab GIT binary patch literal 13950 zcmd6ObyS?qvnD>cCb(;G7~I`GNRS`{3=V_41}8`e?hrIcAUMGxxclHvAhZ$7LI`j6_M5?RGVW1MD!oa{_D9B5{g@J)Bd1(hCBfVUW z1RvR7ZYYlOdM+?9m_Pr#VUw6KNnl_Q@2s_SU3FEI1h)^Flw^LJEn~PAt<5A&Iag+jES<8DngEhTXwamP2%>>M;#Y8EE zJq2F??7^-eN>6(`2Nyw45$eBi1z*~Ky4k5I{}OSv6`_{+qmWWpMV(R#;tZzbVdG^r z4z>f^gB@I5USv7{k#)3!xI$d4ApaZae}?~qf*04SsQjbjzvNd%we+zbjxH+4FW!ztA()_W;QBcYm4044yYe67(|F%%-|3;ZoO6t$T z2vELxXYF7P@o-`McXPnfAXl&m^`CK84gppmkJih=ac~N93NXK1IXM0$ssb^$w($B- zqP$w1Jc8U@f}Gs{LG&eR%t5Z8|EsXMnV<#4*&g&_v$Z|Q63p)CU`b8+k0%ACAa)Sv z7sW5^xc+^=f|QiHGsME$?q$H`t(+vKf{YX|rvNW6E0B%zFS;r!f(i~Ut{?|9u!6J* z^^1SltgX#osDinHrXW66PE&JER!%TCFDt(Vrv)nyml>FcgNvJ!gYTdD(hxJZKT+^! z{=YVaImGM*Ifn(m1vl@X$p06T zytT{AGJE~Kmo&jnf4A(cDgTNeL6F&>eIY_^_9q~~=G1?8TmKjS_-|?cd%lMi_yzR; z5GDW8?gFuJ^#D18B`jZj_umOb_WvD!7m)k^nf-rnHUCBHKe7K`1NZ-t{ol?pvjRC- zf?rZCJM|xX*#9J@e=QCBe_pk}#{OLb`#15ItoWz-Pda}2@J~VqJG?+UzohF|V+K_) zFdR$@(h^#pnTJ_knFTUleeLbutJ!I-DTZvYh|-vd$;_tG9P}FMT5My4n>cYL8tQoY z&JkZG2sI)ETJT=+7@bB0etAQkLY>0GLaE2L4FzLe1*YvCHDo_%K*lbHU9T^MrbMP{ z3|1dsHM_POUbeh@{O$RhF^wo*o)$Oqg4m%RBSM-wUcLZFBlqP-JDDII6!oV=4HuCK zH&U8fMB!D*U!t`CKL}Vi07aj-31BF_Sl|UXXr6~TjwyNi(OF=kVmAk$pfwWN24QaLD7Sd&^I%4&vY(#V4SnF1@@T|0oF)k&a3SPz$b% zHW!_)t5kB-OjTX`4H_UVk6Vla(vNa}YdPa+q}Wl4`5uY>jnMc-q=E8zz?>^CiTiDUONpvmDY}elE*( zMV~_MX1;`V*x^s;>y+*6kuyiz)8GxPcF)zs05ro_CE4OF`cNMyQtmq(2+|{T43oPR>?u;vgsu|`wEwI1Bqb=2=W0aKQInxBIw9?x&A;PrVXj$swHf~_Zv-oC&d_;C+aZH z*kf5!A{+9#2O&BtFF!+&*bc{9-l`#X2|;aaMQY@Yx|8c545g(Dg#L@mNGH9 z0Emich{rvy77Xv}G7?V_h>U;U^1)MZG}ao>*$eF|d{!lRPzh#lAJlp<4)!@6k~l`n zo}e7VL`^)S4%TeZ7{(CBP4hxe@Tps0!4R}tV!l#Xz*XBTxmbPdz?%Af`spGP|b;KyOW zN~CM?8imA1p~T}nqrHY!#dm=|4&i5+2xvHHvUkcFS=7tYQ}U$Lrx@Y>un&3+<(&JE z$?lb-5(1#&^&9NoFXa2*5As5u5n6FJ?V^V)7IQ7+Kc%minDX2CHs*dk1?HU>f#*V2 z58MHl}Y`Gwd~_&$zG(EoN*#izXyv5wW?-zP{c5( zhzBt9{kppfuG^>P{^GI=d(~9n9wycUZXnZ#B)B#5XButGsIVoRakgPTz1;9sK416n z5lVXuHeNM4ITV_3H5fN<` zU!DkB#Ywt-aId^8FZ?>%Hrp11c@azTAdACTr-;WbW{FVk4uZgC%cU>}8Z!9z^z#89 zM8NeUY3Q({<;8PS%4vGoM%}GH^9&H46*ezj_BXJN&3RL`GNV(d91#5?BR3GL>RX+o z4!_Pu6TNy9WTz(3hJHsEOYTT=3>-CL?;|6$gg>`f%(Msz-Krn)!V1B7ZCTE(oVk@` zw3p-$;P*sAi<71=JYAaV#~xIWd`HtJLpYhCD&ZMy12Ae&!+itDq}tOVt%R-5x5&d? z3wD0OjGR-o&92Q**|%59ut=woNcnJ|y&k(l@ADZ&l8TKr$=Stfo_8a>`NsY`lUt;JHa&Z z^CoxeH$QY`Tvc*IT8a7d3~n1(gb0dQZ5wi@zdujhpU4%H_U2>RZ!A2wW%E2=BcG=X zn>~H@%7e?^6NHM7s`fgaPy0KtC}`?9EDuU0f!yVl$aozcCcQW-#wzhE$8f?=spHe~ zGNgQm0z(al1K=*ZwiHqBMSP57`K=MvZu1#J>re8UYgTKnw;Kp5wst-`fDeDmr8am5 zdMUX8n$s#gihS+wwxPedzIof>eVx8!(kvpnieOgdBgNh~6G6+l9^S|_Hvb0n*UE*T zcT00Xo#s$KbS8r7k1XGquXe{{Q-L1u`td((NzEVcsll0G_cH}{wjk1FUq*H7IXwg| zIS<$``JPskH!&Y;ux3q=&m>Bwct+DHL>fEzB{qix<~dnMBAQxBy}s;>Lzm}!DAkHs zklmyG$pB*|2W6wH&NS>x<_l1TrUhbiXN6TyK2JL4Sjn-!*0oleW* zoZ#}j{2l2i6}zCPX`IbKOLj&4{Wf%i>)ZT~$j{ut7PE@gcFo^XRlKxNX+qXszsdC$s zQQ9rFN@TsY*o8kO5v&fXq%8F=G?Rl&Kw{@9BUoa)IC(&m@PeP)27J{&9{7a{{jz}e z>{8*dYDn1#EU?LN20<|$kGa~T)j1i^1()9GF^D?!h5_*mzxjH$e7qwPE13b``#xGuQV4d>+rePvU#2V0WtR zU+0GR=scAaSqxiDsrpJER1$|Yr$R$POn(hyr5P8Y2^ak%Un)y;PZ6Wi;?5awax6_bq0#T6~G~?@`_nHcffCN zt#c*e&4S=R+oJqo;$>S*D3UiGg6;?|zMI@Wta*!|Np5qx%ilGc?#t@DLz-tyJ6oCp zb*u&(X+ONR_V8wK-*|*Us!Z}sS^EU%)`~DD?j6P;f_MH4?fHAJ$L#N?S=HRlPpaIm zELNwlq`Zgoh4Lz&WF0Y2*31F;t=HL;av|>azTYIF$>@(i6V?!@WI!rJ$#3kRGk z`uH60Q649QMEl|y$cH=oDEbE}DmHqbTSGD0S35owR(RKkzTt*UF!b$|5Awv>f5~AS z5jjMDsF4gg6o~7r!RbTJ5*{gc2hu$wdlOky0-Bq2By|D28#GPrDfh2IyDrmFE1TUN zBnu=Gslwt_9KcDxmZh?EGUjNe7S{Yq7D|aX(K=-IiqEh4Yf0u5^LU=$Mti{iio3^@ zOJeei`5o5zLFTn#gndZfDK#9=`>JSaQl<-~sCh2KD5K4=ix%-Dz|aL&BLDgYLDlM% zl+=zBR$DrnCc)0WT*zQ<`T5N@ z?iiLea)?lK@;DX8_M6~zDsdY=$Ua6Cgy({-eA;*~nch&A#2OhDkD=vp-8M_dJc#L83 zyHv(oeEZ0RWz;A1=mgFcbl-eKk_H^oU4z_qlLCTI>%mrI!?EpVU*9@z2w&^?1-y>0 zQ&!K5LxtDcgQO3lADD6$8Ck<}#dKF%ub%y83||o9 zR8)NNC0Z1yXr6fDMb_k}I7MjDZCgt-q3zuUpkrw1%*PUrS~isN_y+D=8A@%qc2;d*B-P>`^qwOgfSb5=ZC^XS5y5^%nM&_V-*pdXcz6F<)Xaz?D<)9!AGEmFZf;(4^q0= za-09@K2$28y0unL_FMz?lCmY^#)sko@Q~5Z1OMuCI82hXvZsJARe{CHizqz1fmK~& zWhiF{+^jL7^~62hQ;5YPcTdhr0X~E$0{8m+tsN3>$Duzt9t5j~7DXU1c*UubeR;|C zmJ206qP+b^EX`ve*J%cD+T9(ObOj&pPP?^vpRZtNjr|ES9DGuLnGY>J$=vRU)?0`H=G?I1XIEjAJnhFIMH{qSv(?Ew3eI1jhod0P2r?ryXhZy0? zO^hI0)GQuBDAi0m-lC9*dW=e16QjBtLLM^h&)othSlB*W1_{Q5!1`Nr*Q9K4%sSg@!6RUTGm|S3F#y*>h^1jDBsvS2Z;f9oakJ5Z2rv?->+{_59nu}3O6DZKJ~ zsGZ0kGg!_{b{=b|rKm<(u~jh48EhFg4!QWjnEjUFmgmN-gQ;Rsk2>!xkWZwt)Li?t z*-p*F5!-P;ku?j>;e7-*h@_P+hp)+_UTBZq4zPCE;WZMM7SV|rF_r#E&k`~+s>X<= z<5DS5MPa%m`SDWt11uJE8OJ2arPmnuKz`3$g;3e4)kjUcpBLABQ5Wco(c-XF^;(Qn zWH_baL@f~b^DF()4-D)8tfX*W#9sjPq)9y|U#k=Zry=#U3i2Hxxy{GeE~czkLi!tx z?&rziRfUroSt<9ydz;O&N8gE^%j^rcnj<2xd*nJ(=QoQ}j1OX)9DaezvGYEeH_{qI zbUtJj0b+Lw*GS~(@#=J7tKrNP4&lANGW1OSpD>ZvW-*ARvsPsRaOjR3BA9n1hQ*F8 z6pHpYX0olp!EDwI&2O36Gs_t0hK`ela&|b!%qLWB54c9S^Z9Bwhi|Fc!lpxPoWyi4 z?b^0s$saFU6OZS`>#ebqNPq9!J2`jc^3CcXrBwpy1`|uT<-0F5sBoU&=U#_YDZLfm z)Qm*OetoH-ZLM5dg2mooJ=?=rRf5kB)1S{Z*G?mekF+BTs2lu6EX*HD60iit$v?^p zAT%A3O-HVK)x!7KO?_|E>D7@H$j5v4WykV;jxaj%Q~f;A&bf?n`6s*5akW|gM0JCL zTbn{k)%;kJO(^eviM)Z`BA*yRsksC;lX(-zWfgFH*d-Rg1SV(0`VRftGn^1_8V)~Z z*N~2_!!yY&g*e4Wuf5yHY_Gj{F?iL=(@+_YMf#<_@(V~yo>7xHzL<%TT&UeMv^1Eu zA>&oiYV&e6qo(O6FLX|h&tXI+pJ17GM0Is3djyj2dt+4cQr?Rted<{KD)R~e(wlKr zP4f@iGvm)WVsutPqy>_S68ObD>gcpf>`*L>@&lV)L8>;3_$7jgjRI84JO~9JfQ;IT z)AUIUMYyjOWj6Ph!PS>_QMsa_#BB`T@|uz%FvL6nnBU%1YaKT~R6yBX!lE3tMZn2!JESqnUuvo|y=v8Imw_blFIsKaMGkw);|73;sh;$H#w#`I>C!}F$ z1S_aO07+VNUQ=5fQM1ZPJdi#SD;aT3Dv3sppwec1G&GaT#A!oanmZUu=b+O%aW+rh zy5%D&`ASf@B}1PDLkc|-kg50kB7XS>3B5mFnGG*gKdqBwxZ?bGano@|PW}eB;;hZZ zLZcsQek+$K>54P*uxGn@yNwvT<3{G^qyY7V(1rApwJbgF=DtIIg|WlnO|dco{*caT z)YvMUa?%Zg$u9zal+KmhuE5=nown5Y!?ZW*B8nBb3aPagzJsrvDIAQ5>`0P+(!$mH zqw0-L6E8143Tj=kO8=fCD%|fNn;#WH!$>ffNoxDWpye;TAjONG;#o+MDqRzRTu%NB zvhU{rTm4*7*hw~`Jmp=JiH?MCv={b>AFW>IR~VM*72@Dlu!k&a$bHod%pgYxbJG#` zvp$&#_Vfv-k}W@Gx~I$Yt7AmNsI z`H!yj11Z}UROJ&l+;|StTtPevUKb(t9=2o>58sNEmfIyeH45K0%$8^nf5C#Zw3o=J zUa8UeDa_k;>U+N_((#8Z3V*0-6QNg!){A`UGLVF8(|c# zd&4&3JtMQbGomIG-=4OfDL;vjGXfR9M4*i?;96X_a~U;zeD!+>-n>4rDt?~dE(0o6 zJkN{fC0@XQpSWsU@vUekRa-2p=d3N82L3JWM7gHW#Wz?vyTx8CzU@R_~gzPkjT3ZvGrbSDyozeDe>7ZRd}W2 zj|wntGo^mF8W8^cb7YqzF>QiRo!XF>g6kxKdqhZPw~wQ`M=D1^cb#CXM(z6)?`y+u@tH82rV+x~Ui>XLStk=DahV z|Mu}~V%dAQb+`AA?F}3diRWl(WnA_8s>%{gnt+{CSo zx4^oqI{?}*bq(1XZB2ISpz^7jdupySS%-Ef=yag@Pd$7_qd1fLgI71CcSDcQ{|ao_q$$M^W3ywg?)aaW@~mP)(i)ny4!0E zBVELy&V6ekqpXuIxKDB@NzcwVYxz=9Jc~4(ligbwi)V3D9 z1O=tY$aC@ie8)SM{Rn^242#4XB5GsBM>!RTH~%EVf=5bsFwn7_Rn_2uJmbdIV4G(d zwBXL=q4OwhKkv$VC&^l%R@~1AQ-ELe6=z1cNS-TRu|(8CM}S>(Pm1jOh*ec@X%c*l zkvw!Ef8>Db`=F4EQ%{Ng=lP82_MO}J7U$i+o^-tyQ)6O!pbFkbxi+bYa-o!3C*zMK zUMFpr2lc<;586$!u0&02L`qRaiYUHq&6d48A8*~?dpX&gG{oNqj2`r-1V)9geE4d^bFjqbHHNNo7xM8kQH}1l{AnwX`Z-GCK*D1tat}Bw~!8M`@{3H{(1U z+faS&TBsx{i$$F&>|(kUBApA5l$`C4WITxYF+!8*3ay*H2 z)B+~Dr>F+_95>2wE+9EtcO zDjrO{+lbARaFoJK|e4io#vKzs@by&e^OCMLTC>x4tJ% zTS=ZU{#A&Nv?w?(MU*0Ozw(ubA%@=08ET1eoEaNxktwjwb*A46nur~keH$~Pg#1ko z<1V96^S@XFUT+l@ah3EpJ#fRzKF+w zUe31Ru~r(gg|6R^YGN+Mv|g`<77pQ`KxaW=%zDemHX=n{cKQuTwrr?f7bhb7M5(KN zlir~8o?k=o@T1=itVJ6z!MCxcG04@QQ?r6^az6C1D6iwKskwQ%p6d%BQsZ5a=J6Pn z$=a~Tu8))8>P{LRfAx-~F)+j{&`tjKC@!rJzkW|RTc#sV{F6^`#gJbwLUIOzxajmcnlo*W zM2=T|Zk(3af$xKF&(PRJ&37m0@(9z9vw7O^$7jvz`@xgZw6T%&{w!v{>GIBJk7O5~ z>szZ8-Ky|4(EqM(AKGYvrsSdb^P%T9^Hy0@b0nE^rm(Zr@)tJ}fwy zZ$U18L9O;fZfaG3#hDk&VvJc=x`FUp1?HK0yNoAU-EA#;0efIZZPKw*+(0z}eUsyK zB@F86DZN7`rm+6jkBl3aI4W>c5;={RjP_&7R?Y*X1al>tsf<6$I~VaLMX zldP{w#LqdS2(-A5bGUf0cR5x(tcsrY(dUZ7cZa8}zAAoSB?-&kR5&Re2_=@SbaqbJ zkvoL{F)<#QRn=q*2PT?}ce;;Ov)mf%{Sf-dVkDU3h6ULvRf7DQggk^D``tOS-7i3( zjqY4R2Q3j^AMY(8Z`AGta4ROwkko#YtmOvzBl#8f2of5X2|_L$S6-2~l5t%cA3ELh zULtV9Tr&=|`gt>~5Gl7PLUAe_vN?cB{oE&*d2@Ab6YJ>$58uN?`0I;bhh5XkG11_d zeKt{%z#0_MY=kjmkh%}N|FYGl!OU!#y%rF?wK?*o`eSq=5G9F|vCxax=&cYC**pDPX!Y_wr zp0sx|6yQv4F^z*eLd<7!kQhJ}vmUmJ584;+0M`Iv9}}9vzn){iXn^a^f;~hV2nck? zt*=~X<7ldk5jsYEb1V9`8_XiJyMk@J(Pj!g4keqa1L~`nS^i>2m+HJ;ZbE`p^3aE( zxhMy4xUQQ6h}50wuHF>(hcT*{R>1;Wg*5l%9mi~*5iq(%2DE<6=hbkqnY)sD5p5K9 zG>2eXadq>iu9@*wu2o)h(#-|8l5F~>XO{Q*z50Cp5{RsybZ>j8n~{1Qgm8Ciafj0d z|~ zJ&{)f*VaL)i0d&2H8arXiZeZHI_Cn*f&f9_j`;poSpLR3ew1L9NNrU|t$=x!jzY+x zh({VFJhNaymYlPdu(0)waS?j^*BK>!`#aS8B+P)ccwvufOUlNobIxEYIU0d-v1&$J zU)o2)?e&b;0LKD!HkN_?Zg( zc^qbAa^su>DM6YFmd(Q)!kBr<|AP;}gPs_wy<}|?pyV0YU@X~v=t2lk>S4fUm=A;~ z!_}wX=A$<|MUks3Nywp)sr2YMv`ZM|)Y^$cGrlsGy9|GJwS^ND`TdQtAImm{YeS!_!g?vIPNfpY^y2TEr ziVmo-=i5!E-ChYE!R@tnrF`0X0|(VQ;FHxZtg#h()Y4h;11rUmHn zwepPJ`UxNr;A+luHxYncu)p3_pu1+LroaYGMcJ7d+E7Ux$F>ouPw@#=C`pdT2@+~ z(C1%s8PsB5-@X46j}_OhC>-6PAr5l$S7iF;~r=KKr2 zvOesYOA`2G{A_s|5C%kixW8~E1?~Fkxqs>x?VI)%Ye(|QO3ShXVJO5GLnv8Bg)?+DcBPi!! zI~MUuyToc=YD5gF9I08sV)ay!%~=P7OSbeaPrL?or})EHlWTWX&!4o=(6ypFnqHJT zK!IZ)m!^YPk}_S(UMMaDJTt;QkXP{mY0V?-aeB=s)j{#hr2SU%2*C+1K?rx+!Y#773bXgi!Ld9M)X?lAET z^Q%vcSQge>=R7mML7@ItA-JTsXvh0FxXt_zMybi+jiidC^A_wC3G;_xX$(|$606zb zq37kc_w^F8d;E?HSrqT!gstpjO_dFEy9~?TNLcu(9w6zbKNr|k!PWri(5|%@SrKf0 zdlYxlKy`D0z(UU|Vh{OIwhHXO_5|3Af+Uvqy>Mq<(JKc@ASw zn%o9TM>gS*1i5?%FI7eN0~x(uh3sy!_@z9H=%PuHX8dEyuqce6bv>)iM~+XeoyruQ zi(ctIBthTuH_wH+1}(hD4QxnlJ7%LP)YBbeG;IZIm1OrOtzScl2B)uacQ9?O^HZ8I z&aguX&fJrY%HBeNV*|;jK<>IMQ_ELlmK~TTiW4N%@q6P>)xl|}5z*3uYLzHjv2A9K zgcirW-c_NH2ZT-eb8ix|=p~Yu64rPPZ>`)e*Cek|t|{1vfGm3-XC0E3*_K#%f3C&e zm+}63P~~tG;!6>CbrvT90+Y#7;HhcVI~Zom zsx=54U|rxNb_e61B3Z7EC7cqCHZT$M@CEtW#L;ER0Uv#~ZP7b8k3O%RmqOb#y`3-O zGyDX00m6t<4{9HDKmO7>Cq;QE&-eOM%3EN+Q!Nf#&QsaWbKzP=p(?~z69DijP4n(y zqL*sQ;79HtGO)P6{%D$Q}Na`1q%`ykTDFh29;#V=m~p2MXLf z-B3Lj>;Yzn*DrU}z3>MT%-18JN{R!<#4_e!BgT}GZC+=Rk#`?FnlPI*&)@ygvhzv= z_hTiqF!B}eaGUw$khw4!A#js8t+``ZjFPX}r~NOc0)`(g$p1LC-xYclcJrc}wps=I i|HsPq|7&&qGnQ@MiNzt+JowK)#1v#yrN2py?s%W_h49srP4Yfw}(De7bEmuIErQNP*7y2|sF%I`DD zVo9vUF~Sz){pL~TzO&$Hk!>*gFTTd9ubEXGH{{`5-nAoKTPsk14KsZ%XYCE6t-2X4 zJ5Ryf7DS}-EUbNDH;34}y#ICX{(1gTSm8Q+I>OrnxLDN$JM;k0FZFZ0flzrJ+G|4I zJkLD*W8sBuuhr307e}Id1Y<8|@M8){4^F|>@A0~!a!4%esfV}4*IID~od=@K&El;+ zf#Kd1J`H3rotnqbS)lt!Ct7wi2rIJl`3rj*neV`x`{PL7O5tVKC_eUoK|1*jUq`dJ zdH5R6?>b$Szs~gs<6{`^Nn!F;26K}$m`F||^E#{Q7vozi8gXTR1as+m-kSUtSXuQX zL+sFHK9hp=2jrdP`$Q_6aHswT#uM-UaG#U!al1aI&O~CI_xFDPF7e6vi9+Q8C0p;s zqjP;Qwwy%s#uJK@b%&30f!zJoj8eW+4URu|#+nIza}fHbpxRg7{V};`mU?)%XV!9_ zn4717jq~~jb$_|%{^F(oR1ccFnN4{3A@0Fy=}SxcRQj(d@8r>pV;p1Vaae135& V(ZVXC4@-!GCI2Uh73Nof=r>8fg{lAm literal 0 HcmV?d00001 diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..56214b42a224febb69a589cb954036ec1169b468 GIT binary patch literal 6512 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 Date: Fri, 27 Sep 2019 13:19:11 +0100 Subject: [PATCH 004/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7093b140e..92c7ff02d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses. - ## Currently supported databases: - [SnowFlake](https://www.snowflake.com/about/) @@ -17,6 +16,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" + revision: v0.1-pre ``` And run ```dbt deps``` From 7d11591dbbe45ddab63d181b1dec9ae4fcc515a1 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 14:28:04 +0100 Subject: [PATCH 005/157] Added header image to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 92c7ff02d..d045051dd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ **CURRENTLY IN PRE-RELEASE, STAY TUNED FOR AN OFFICAL ANNOUNCEMENT AND FULL DOCUMENTATION** +

+ +

+ # dbtvault by [Data-Vault](www.data-vault.co.uk) dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses. From afb016e738f24f26c23ef6c85f97ba6b27a191dc Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 14:31:21 +0100 Subject: [PATCH 006/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d045051dd..055c7786b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses. ## Currently supported databases: -- [SnowFlake](https://www.snowflake.com/about/) +- [snowflake](https://www.snowflake.com/about/) ## Installation From f4bdcea73858b7f8d01cef2f6ef1dc63feb07612 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 14:33:01 +0100 Subject: [PATCH 007/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 055c7786b..f365fcb6e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

-# dbtvault by [Data-Vault](www.data-vault.co.uk) +# dbtvault by [Data-Vault](https://www.data-vault.co.uk) dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses. From 51ea2253487ad90fe7e95809456a12d00cf2e15a Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 14:39:11 +0100 Subject: [PATCH 008/157] Updated installation --- docs/gettingstarted.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 3143aa6f4..5a6e85c61 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -6,6 +6,7 @@ Add the following to your ```packages.yml```: packages: - git: "https://github.com/Datavault-UK/dbtvault" + revision: v0.1-pre ``` And run ```dbt deps``` From 0d1e537b1c05b8505b224f6fd058d233a91ccda3 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 14:52:06 +0100 Subject: [PATCH 009/157] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f365fcb6e..cd105fe0a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.1-pre ``` And run ```dbt deps``` From 21e6c568efc274cc779203035e7d564da191ede3 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 14:52:47 +0100 Subject: [PATCH 010/157] Removed version from master readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f365fcb6e..cd105fe0a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.1-pre ``` And run ```dbt deps``` From c0dc91e31316a2a6b8ae8fd406af412522751419 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 14:54:52 +0100 Subject: [PATCH 011/157] Removed version from master RTD --- docs/gettingstarted.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 5a6e85c61..3143aa6f4 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -6,7 +6,6 @@ Add the following to your ```packages.yml```: packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.1-pre ``` And run ```dbt deps``` From ae14655b754d3f23408ad8cd67fd417cc8986ffe Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 15:46:32 +0100 Subject: [PATCH 012/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cd105fe0a..444afea74 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ And run {%- set metadata = ... -%} -{%- set hash_model = ... -%} +{%- set source = ... -%} {{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, From b76808ef2ee9371d8c537799849dec7b6e05df52 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 16:54:34 +0100 Subject: [PATCH 013/157] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 444afea74..ab5b96be4 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@

+[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) + # dbtvault by [Data-Vault](https://www.data-vault.co.uk) dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses. From d02d4ee36bc11dc63bb90563f85fad1c6ed41ba4 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 21:26:53 +0100 Subject: [PATCH 014/157] Updated wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab5b96be4..b54387266 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -# dbtvault by [Data-Vault](https://www.data-vault.co.uk) +# dbtvault by [Datavault](https://www.data-vault.co.uk) dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses. From 6c392898378dded42de9fdffef318e51301cfd9f Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 22:28:56 +0100 Subject: [PATCH 015/157] Added cube logo to navigation bar --- docs/{ => assets}/images/docs-banner.png | Bin docs/{ => assets}/images/favicon.ico | Bin docs/{ => assets}/images/logo.png | Bin docs/index.md | 3 +- docs/stylesheets/cube.css | 12 ++++++++ mkdocs.yml | 14 +++++---- theme/main.html | 35 +++++++++++++++++++++++ 7 files changed, 57 insertions(+), 7 deletions(-) rename docs/{ => assets}/images/docs-banner.png (100%) rename docs/{ => assets}/images/favicon.ico (100%) rename docs/{ => assets}/images/logo.png (100%) create mode 100644 docs/stylesheets/cube.css create mode 100644 theme/main.html diff --git a/docs/images/docs-banner.png b/docs/assets/images/docs-banner.png similarity index 100% rename from docs/images/docs-banner.png rename to docs/assets/images/docs-banner.png diff --git a/docs/images/favicon.ico b/docs/assets/images/favicon.ico similarity index 100% rename from docs/images/favicon.ico rename to docs/assets/images/favicon.ico diff --git a/docs/images/logo.png b/docs/assets/images/logo.png similarity index 100% rename from docs/images/logo.png rename to docs/assets/images/logo.png diff --git a/docs/index.md b/docs/index.md index ff0412585..e50cc9e17 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,2 +1 @@ -# Welcome to dbtvault -![alt text](images/docs-banner.png "dbtvault - The dbt package for Data Vault 2.0") \ No newline at end of file +# The dbt package for Data Vault 2.0 diff --git a/docs/stylesheets/cube.css b/docs/stylesheets/cube.css new file mode 100644 index 000000000..3aef57fec --- /dev/null +++ b/docs/stylesheets/cube.css @@ -0,0 +1,12 @@ +/* Additional CSS to add styling to the navigation menu cube +and remove edit button */ + +.nav-cube img { + width: 110px; + padding: 0 .6rem; + margin-bottom: 20px; +} + +.md-content__icon { + display: none; +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index b1d0684f1..f32ee093b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,15 +1,16 @@ site_name: dbtvault theme: - name: material - logo: 'images/logo.png' - favicon: 'images/favicon.ico' + name: 'material' + custom_dir: 'theme' + logo: 'assets/images/logo.png' + favicon: 'assets/images/favicon.ico' palette: primary: 'black' accent: 'indigo' highlightjs: true hljs_languages: - yaml - - + repo_name: 'Datavault-UK/dbtvault' repo_url: 'https://github.com/Datavault-UK/dbtvault' @@ -26,4 +27,7 @@ extra: - type: 'linkedin' link: 'https://www.linkedin.com/company/business-thinking-limited' - type: 'facebook' - link: 'https://www.facebook.com/DataVaultUK/' \ No newline at end of file + link: 'https://www.facebook.com/DataVaultUK/' + +extra_css: + - 'stylesheets/cube.css' \ No newline at end of file diff --git a/theme/main.html b/theme/main.html new file mode 100644 index 000000000..b3f547698 --- /dev/null +++ b/theme/main.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + + +{% block site_nav %} + + +{% if nav %} +
+ +
+
+ {% include "partials/nav.html" %} +
+
+
+{% endif %} + + +{% if page.toc %} +
+
+
+ {% include "partials/toc.html" %} +
+
+
+{% endif %} +{% endblock %} \ No newline at end of file From c1889aa1e47dc8db4924d3ed1de3196ed0de0141 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 27 Sep 2019 22:47:47 +0100 Subject: [PATCH 016/157] Fixed nav logo Changed it to a banner --- docs/stylesheets/cube.css | 2 +- mkdocs.yml | 1 + theme/main.html | 14 +------------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/docs/stylesheets/cube.css b/docs/stylesheets/cube.css index 3aef57fec..d8693672b 100644 --- a/docs/stylesheets/cube.css +++ b/docs/stylesheets/cube.css @@ -2,7 +2,7 @@ and remove edit button */ .nav-cube img { - width: 110px; + width: 90%; padding: 0 .6rem; margin-bottom: 20px; } diff --git a/mkdocs.yml b/mkdocs.yml index f32ee093b..667671611 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,7 @@ theme: name: 'material' custom_dir: 'theme' logo: 'assets/images/logo.png' + banner: 'assets/images/docs-banner.png' favicon: 'assets/images/favicon.ico' palette: primary: 'black' diff --git a/theme/main.html b/theme/main.html index b3f547698..2f1f1f4b8 100644 --- a/theme/main.html +++ b/theme/main.html @@ -10,7 +10,7 @@
@@ -20,16 +20,4 @@
{% endif %} - - -{% if page.toc %} -
-
-
- {% include "partials/toc.html" %} -
-
-
-{% endif %} {% endblock %} \ No newline at end of file From 08a35ab334e4ec520a50a6dec42a2e5653a54e3c Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Sun, 29 Sep 2019 18:15:28 +0100 Subject: [PATCH 017/157] Docs updated - Macro page --- README.md | 5 +- docs/assets/images/docs-banner.png | Bin docs/assets/images/favicon.ico | Bin docs/assets/images/logo.png | Bin docs/gettingstarted.md | 6 + docs/images/logo.png | Bin 0 -> 6512 bytes docs/macros.md | 256 +++++++++++++++++++++++++++++ docs/requirements.txt | 3 +- docs/table-types/hubs.md | 0 docs/table-types/links.md | 0 docs/table-types/satellites.md | 0 macros/utility/cast.sql | 8 +- mkdocs.yml | 13 +- 13 files changed, 281 insertions(+), 10 deletions(-) mode change 100755 => 100644 docs/assets/images/docs-banner.png mode change 100755 => 100644 docs/assets/images/favicon.ico mode change 100755 => 100644 docs/assets/images/logo.png create mode 100755 docs/images/logo.png create mode 100644 docs/macros.md create mode 100644 docs/table-types/hubs.md create mode 100644 docs/table-types/links.md create mode 100644 docs/table-types/satellites.md diff --git a/README.md b/README.md index b54387266..f3593c769 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses. Add the following to your ```packages.yml``` + ```bash packages: @@ -26,8 +27,6 @@ packages: And run ```dbt deps``` -[Read more on package installation](https://docs.getdbt.com/docs/package-management) - ## Usage 1. Create a model for your hub, link or satellite @@ -49,4 +48,4 @@ And run Please open an issue first to discuss what you would like to change. ## License -[Apache 2.0](https://choosealicense.com/licenses/apache-2.0/) +[Apache 2.0](https://choosealicense.com/licenses/apache-2.0/) \ No newline at end of file diff --git a/docs/assets/images/docs-banner.png b/docs/assets/images/docs-banner.png old mode 100755 new mode 100644 diff --git a/docs/assets/images/favicon.ico b/docs/assets/images/favicon.ico old mode 100755 new mode 100644 diff --git a/docs/assets/images/logo.png b/docs/assets/images/logo.png old mode 100755 new mode 100644 diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 3143aa6f4..ce6ac101d 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -1,3 +1,9 @@ +# Prerequisites + +!!! note + These requirements are subject to change as we improve the package. + + # Installation Add the following to your ```packages.yml```: diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..56214b42a224febb69a589cb954036ec1169b468 GIT binary patch literal 6512 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 | +| pairs: columns | Single column string or list of columns | check_circle | +| pairs: alias | A string | check_circle | + + +#### Usage + +```yaml +{{ dbtvault.gen_hashing([('CUSTOMERKEY', 'CUSTOMER_PK'), + (['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF')]) }} +``` + +#### Output + +```mysql +CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, +CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) + AS BINARY(16)) AS HASHDIFF +``` + +___ + +### add_columns + +A simple macro for generating sequences of the following SQL: +```mysql +column AS alias +``` + +#### Parameters + +| Parameter | Description | Required? | +| ------------- | ---------------- | -------------------------------------------------------- | +| pairs | A list of tuples | check_circle | + +#### Usage + +```yaml +{{ dbtvault.add_columns([('PARTKEY', 'PART_ID'), + ('PART_NAME', 'NAME'), + ('PART_TYPE', 'TYPE'), + ('PART_SIZE', 'SIZE'), + ('PART_RETAILPRICE', 'RETAILPRICE'), + ('LOADDATE', 'LOADDATE'), + ('SOURCE', 'SOURCE')]) }} +``` + +#### Output + +```mysql +PARTKEY AS PART_ID, +PART_NAME AS NAME, +PART_TYPE AS TYPE, +PART_SIZE AS SIZE, +PART_RETAILPRICE AS RETAILPRICE, +LOADDATE AS LOADDATE, +SOURCE AS SOURCE +``` + +___ + +### staging_footer + +A macro used in creating source/hashing models to complete a staging layer model. + +```mysql +,LOADDATE AS LOADDATE,'SOURCE' AS SOURCE FROM DV_PROTOTYPE_DB.SRC_TEST_STG.test_stg_lineitem +``` + +#### Parameters + +| Parameter | Description | Required? | +| ------------- | ----------------------------------------- | -------------------------------------------------------- | +| loaddate | Name for loaddate column | clear | +| source | Source column value for each record | clear | +| source_table | Fully qualified table name | check_circle | + +#### Usage + +```yaml +{{- dbtvault.staging_footer('LOADDATE', + 'SOURCE', + source_table='MYDATABASE.MYSCHEMA.MYTABLE') }} +``` + +#### Output + +```mysql +,LOADDATE AS LOADDATE,'SOURCE' AS SOURCE FROM MYDATABASE.MYSCHEMA.MYTABLE +``` + +___ + +### cast + +A macro for generating cast sequences: + +```mysql +CAST(prefix.column AS type) AS alias +``` + +#### Parameters + +| Parameter | Description | Required? | +| ---------------- | ----------------------------- | -------------------------------------------------------- | +| columns | Triples or strings | check_circle | +| prefix | A string | clear | + +#### Usage + +!!! note + As shown in the snippet below, columns must be provided as a list. + The collection of items in this list can be any combination of: + + - ```(column, type, alias) ``` 3-tuples + - ```[column, type, alias] ``` 3-item lists + - ```'DOB'``` Single strings. + +```yaml + +{%- set tgt_pk = ['PART_PK', 'BINARY(16)', 'PART_PK'] -%} + +{{ dbtvault.cast([tgt_pk, + 'DOB', + ('PART_PK', 'NUMBER(38,0)', 'PART_ID'), + ('LOADDATE', 'DATE', 'LOADDATE'), + ('SOURCE', 'VARCHAR(15)', 'SOURCE')], + 'stg') }} +``` + +#### Output + +```mysql +CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, +stg.DOB, +CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, +CAST(stg.LOADDATE AS DATE) AS LOADDATE, +CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +``` + +___ + +### md5_binary + +!!! warning + This macro ***should not be*** used for any kind of password obfuscation or security purposes, the intended use is for creating checksum-like fields only. + + [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) +A macro for generating hashing SQL for columns: +```sql +CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias +``` + +- Can provide multiple columns as a list to create a concatenated hash +- Casts a column as ```VARCHAR```, transforms to ```UPPER``` case and trims whitespace +- ```'^^'``` Accounts for null values with a double caret +- ```'||'``` Concatenates with a double pipe + +#### Parameters + +| Parameter | Description | Required? | +| ---------------- | ---------------------------------------------- | -------------------------------------------------------- | +| columns | Single column string or list of columns | check_circle | +| alias | A string | check_circle | + +#### Usage + +```yaml +{{ dbtvault.md5_binary('CUSTOMERKEY', 'CUSTOMER_PK') }}, +{{ dbtvault.md5_binary(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF') }} +``` + +!!! tip + [gen_hashing](#gen_hashing) may be used to simplify the hashing process and generate multiple hashes with one macro. + +#### Output + +```mysql +CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, +CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) + AS BINARY(16)) AS HASHDIFF +``` + +___ + +### prefix + +A macro for quickly prefixing a list of columns with a string: +```mysql +a.column1, a.column2, a.column3, a.column4 +``` + +#### Parameters + +| Parameter | Description | Required? | +| ---------------- | ----------------------------- | -------------------------------------------------------- | +| columns | A list of column names | check_circle | +| prefix_str | A string | check_circle | + +#### Usage + +```yaml +{{ dbtvault.prefix(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'a') }} +{{ dbtvault.prefix(['CUSTOMERKEY'], 'a') +``` + +!!! Note + Single columns must be provided as a 1-item list, as in the second example above. + +#### Output + +```mysql +a.CUSTOMERKEY, a.DOB, a.NAME, a.PHONE +a.CUSTOMERKEY +``` + +## Table templates \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 10e56e782..efab1190b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ mkdocs-material==4.4.2 -mkdocs-minify-plugin==0.2.1 \ No newline at end of file +mkdocs-minify-plugin==0.2.1 +pygments \ No newline at end of file diff --git a/docs/table-types/hubs.md b/docs/table-types/hubs.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/table-types/links.md b/docs/table-types/links.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/table-types/satellites.md b/docs/table-types/satellites.md new file mode 100644 index 000000000..e69de29bb diff --git a/macros/utility/cast.sql b/macros/utility/cast.sql index 6826c5147..4f368ebfa 100644 --- a/macros/utility/cast.sql +++ b/macros/utility/cast.sql @@ -14,10 +14,10 @@ {#- Output String if just a string -#} {%- if column is string -%} - {%- if prefix -%} - {{ dbtvault.prefix([column], prefix) }} - {%- else -%} - {{column}} + {% if prefix %} + {{ dbtvault.prefix([column], prefix) }} + {%- else %} + {{ column }} {%- endif -%} {#- Recurse if a list of lists (i.e. multi-column key) -#} diff --git a/mkdocs.yml b/mkdocs.yml index 667671611..128cd0eab 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,8 +9,6 @@ theme: primary: 'black' accent: 'indigo' highlightjs: true - hljs_languages: - - yaml repo_name: 'Datavault-UK/dbtvault' repo_url: 'https://github.com/Datavault-UK/dbtvault' @@ -18,9 +16,16 @@ repo_url: 'https://github.com/Datavault-UK/dbtvault' nav: - Home: 'index.md' - Getting Started: 'gettingstarted.md' + - Macros: 'macros.md' + - Table types: + - Hubs: 'table-types/hubs.md' + - Links: 'table-types/links.md' + - Satellites: 'table-types/satellites.md' extra: social: + - type: 'globe' + link: 'www.data-vault.co.uk' - type: 'github' link: 'https://github.com/Datavault-UK/' - type: 'twitter' @@ -30,5 +35,9 @@ extra: - type: 'facebook' link: 'https://www.facebook.com/DataVaultUK/' +markdown_extensions: + - codehilite + - admonition + extra_css: - 'stylesheets/cube.css' \ No newline at end of file From 4134baf063c09995a7606292e351746aef7465f9 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Sun, 29 Sep 2019 18:48:02 +0100 Subject: [PATCH 018/157] Index updated --- docs/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/index.md b/docs/index.md index e50cc9e17..017c48eac 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,5 @@ # The dbt package for Data Vault 2.0 + +Full documentation coming soon, please provide feedback on our github in the meantime! + +Thank you \ No newline at end of file From cb3efbcc54681dc0aa63d9e0452301f3d6addd19 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 15:53:45 +0100 Subject: [PATCH 019/157] Removed unused parameter --- macros/sat_template.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macros/sat_template.sql b/macros/sat_template.sql index de907847e..ebec12017 100644 --- a/macros/sat_template.sql +++ b/macros/sat_template.sql @@ -3,7 +3,7 @@ tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source, - src_table, source) -%} + source) -%} SELECT DISTINCT {{ dbtvault.cast([tgt_hashdiff, tgt_pk, tgt_payload, tgt_ldts, tgt_eff, tgt_source], 'e') }} FROM {{ source[0] }} AS e @@ -28,4 +28,4 @@ ON {{ dbtvault.prefix([tgt_hashdiff|last], 'src') }} = {{ dbtvault.prefix([src_h WHERE {{ dbtvault.prefix([tgt_hashdiff|last], 'src') }} IS NULL {%- endif -%} -{% endmacro %} \ No newline at end of file +{% endmacro %} From 9b96e89b44a732d5da83a2edab2adb3a795a1d7c Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 16:33:05 +0100 Subject: [PATCH 020/157] [Docs] Completed Macro page - Renamed utility macros to supporting macros - Moved staging macros to a staging folder - Updated build requirements for docs - Added Licence page - Re-organised menu - Moved assets - Added copyright to footer --- docs/LICENSE.md | 202 ++++++ docs/assets/images/docs-banner.png | Bin docs/assets/images/favicon.ico | Bin docs/assets/images/logo.png | Bin docs/gettingstarted.md | 4 +- docs/images/logo.png | Bin 6512 -> 0 bytes docs/macros.md | 575 +++++++++++++++--- docs/requirements.txt | 3 +- macros/{internal => staging}/add_columns.sql | 0 macros/{internal => staging}/gen_hashing.sql | 0 .../{internal => staging}/staging_footer.sql | 0 macros/{utility => supporting}/cast.sql | 0 macros/{utility => supporting}/md5_binary.sql | 0 macros/{utility => supporting}/prefix.sql | 0 mkdocs.yml | 12 +- theme/main.html | 13 + 16 files changed, 709 insertions(+), 100 deletions(-) create mode 100644 docs/LICENSE.md mode change 100644 => 100755 docs/assets/images/docs-banner.png mode change 100644 => 100755 docs/assets/images/favicon.ico mode change 100644 => 100755 docs/assets/images/logo.png delete mode 100755 docs/images/logo.png rename macros/{internal => staging}/add_columns.sql (100%) rename macros/{internal => staging}/gen_hashing.sql (100%) rename macros/{internal => staging}/staging_footer.sql (100%) rename macros/{utility => supporting}/cast.sql (100%) rename macros/{utility => supporting}/md5_binary.sql (100%) rename macros/{utility => supporting}/prefix.sql (100%) diff --git a/docs/LICENSE.md b/docs/LICENSE.md new file mode 100644 index 000000000..427417b60 --- /dev/null +++ b/docs/LICENSE.md @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/assets/images/docs-banner.png b/docs/assets/images/docs-banner.png old mode 100644 new mode 100755 diff --git a/docs/assets/images/favicon.ico b/docs/assets/images/favicon.ico old mode 100644 new mode 100755 diff --git a/docs/assets/images/logo.png b/docs/assets/images/logo.png old mode 100644 new mode 100755 diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index ce6ac101d..3af72dd21 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -1,10 +1,10 @@ -# Prerequisites +## Prerequisites !!! note These requirements are subject to change as we improve the package. -# Installation +## Installation Add the following to your ```packages.yml```: diff --git a/docs/images/logo.png b/docs/images/logo.png deleted file mode 100755 index 56214b42a224febb69a589cb954036ec1169b468..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6512 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 | +| prefix | A string | clear | + +#### Usage + +!!! note + As shown in the snippet below, columns must be provided as a list. + The collection of items in this list can be any combination of: + + - ```(column, type, alias) ``` 3-tuples + - ```[column, type, alias] ``` 3-item lists + - ```'DOB'``` Single strings. + +```yaml + +{%- set tgt_pk = ['PART_PK', 'BINARY(16)', 'PART_PK'] -%} + +{{ dbtvault.cast([tgt_pk, + 'DOB', + ('PART_PK', 'NUMBER(38,0)', 'PART_ID'), + ('LOADDATE', 'DATE', 'LOADDATE'), + ('SOURCE', 'VARCHAR(15)', 'SOURCE')], + 'stg') }} +``` + +#### Output + +```mysql +CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, +stg.DOB, +CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, +CAST(stg.LOADDATE AS DATE) AS LOADDATE, +CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +``` + +___ + +### md5_binary + +!!! warning + This macro ***should not be*** used for cryptographic purposes + + The intended use is for creating checksum-like fields only, so that a record change can be detected. + + [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) + +A macro for generating hashing SQL for columns: +```sql +CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias +``` + +- Can provide multiple columns as a list to create a concatenated hash +- Casts a column as ```VARCHAR```, transforms to ```UPPER``` case and trims whitespace +- ```'^^'``` Accounts for null values with a double caret +- ```'||'``` Concatenates with a double pipe + +#### Parameters + +| Parameter | Description | Type | Required? | +| ---------------- | ---------------------------------------------- | ----------- | -------------------------------------------------------- | +| columns | Columns to hash on | String/List | check_circle | +| alias | The name to give the hashed column | String | check_circle | + +#### Usage + +```yaml +{{ dbtvault.md5_binary('CUSTOMERKEY', 'CUSTOMER_PK') }}, +{{ dbtvault.md5_binary(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF') }} +``` + +!!! tip + [gen_hashing](#gen_hashing) may be used to simplify the hashing process and generate multiple hashes with one macro. + +#### Output + +```mysql +CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, +CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) + AS BINARY(16)) AS HASHDIFF +``` + +___ + +### prefix + +A macro for quickly prefixing a list of columns with a string: +```mysql +a.column1, a.column2, a.column3, a.column4 +``` + +#### Parameters + +| Parameter | Description | Type | Required? | +| ---------------- | ----------------------------- | ------ | -------------------------------------------------------- | +| columns | A list of column names | List | check_circle | +| prefix_str | The prefix for the columns | String | check_circle | + +#### Usage + +```yaml +{{ dbtvault.prefix(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'a') }} +{{ dbtvault.prefix(['CUSTOMERKEY'], 'a') +``` + +!!! Note + Single columns must be provided as a 1-item list, as in the second example above. + +#### Output -Internal macros support the utility macros provided in this package. -They are used in the utility macros to process provided metadata and it should not be necessary to use them directly. +```mysql +a.CUSTOMERKEY, a.DOB, a.NAME, a.PHONE +a.CUSTOMERKEY +``` -## Utility +___ -Utility macros are helper macros for use in models. These macros are used extensively in the [table template](#table-templates) macros +## Staging Macros +These macros are intended for use in the staging layer ___ ### gen_hashing !!! warning - This macro ***should not be*** used for any kind of password obfuscation or security purposes, the intended use is for creating checksum-like fields only. + This macro ***should not be*** used for cryptographic purposes. + + The intended use is for creating checksum-like fields only, so that a record change can be detected. [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) @@ -29,11 +161,11 @@ CAST(MD5_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(16)) AS alias2 #### Parameters -| Parameter | Description | Required? | -| ---------------- | ---------------------------------------------- | -------------------------------------------------------- | -| pairs | A pair: (column, alias) | check_circle | -| pairs: columns | Single column string or list of columns | check_circle | -| pairs: alias | A string | check_circle | +| Parameter | Description | Type | Required? | +| ---------------- | ---------------------------------------------- | ------ | -------------------------------------------------------- | +| pairs | (column, alias) pair | Tuple | check_circle | +| pairs: columns | Single column string or list of columns | String | check_circle | +| pairs: alias | The alias for the column | String | check_circle | #### Usage @@ -65,9 +197,9 @@ column AS alias #### Parameters -| Parameter | Description | Required? | -| ------------- | ---------------- | -------------------------------------------------------- | -| pairs | A list of tuples | check_circle | +| Parameter | Description | Type | Required? | +| ------------- | ----------------------------------- | -------------- | ------------------------------------------------------------------ | +| pairs | Collection of (column, alias) pairs | List of tuples | check_circle | #### Usage @@ -97,7 +229,7 @@ ___ ### staging_footer -A macro used in creating source/hashing models to complete a staging layer model. +Used in creating source/hashing models to complete a staging layer model. ```mysql ,LOADDATE AS LOADDATE,'SOURCE' AS SOURCE FROM DV_PROTOTYPE_DB.SRC_TEST_STG.test_stg_lineitem @@ -105,11 +237,11 @@ A macro used in creating source/hashing models to complete a staging layer model #### Parameters -| Parameter | Description | Required? | -| ------------- | ----------------------------------------- | -------------------------------------------------------- | -| loaddate | Name for loaddate column | clear | -| source | Source column value for each record | clear | -| source_table | Fully qualified table name | check_circle | +| Parameter | Description | Type | Required? | +| ------------- | ----------------------------------------- | ------ | -------------------------------------------------------- | +| loaddate | Name for loaddate column | String | clear | +| source | Source column value for each record | String | clear | +| source_table | Fully qualified table name | String | check_circle | #### Usage @@ -127,130 +259,383 @@ A macro used in creating source/hashing models to complete a staging layer model ___ -### cast +## Table templates -A macro for generating cast sequences: +These macros form the core of the package and can be called in your models to build the tables for your Data Vault. +___ -```mysql -CAST(prefix.column AS type) AS alias +### hub_template + +Creates a hub with provided metadata. + +```mysql +dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) ``` #### Parameters -| Parameter | Description | Required? | -| ---------------- | ----------------------------- | -------------------------------------------------------- | -| columns | Triples or strings | check_circle | -| prefix | A string | clear | +| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | +| ------------- | --------------------------------------------------- | -------------------- | -------------------- | ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | List | check_circle | +| src_nk | Source natural key column | String | List | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| tgt_cols | Complete list of all source columns (pre-aliasing) | List | List | check_circle | +| tgt_pk | Target primary key column | List | List | check_circle | +| tgt_nk | Target natural key column | List | List | check_circle | +| tgt_ldts | Target loaddate timestamp column | List | List | check_circle | +| tgt_source | Name of the column which will contain the source ID | List | List | check_circle | +| source | Staging model reference or table name | List | List | check_circle | #### Usage -!!! note - As shown in the snippet below, columns must be provided as a list. - The collection of items in this list can be any combination of: +``` yaml tab="Single-Source" + +hub_customer.sql: + +{{- config(...) -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_nk = 'CUSTOMER_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = [src_pk, src_nk, src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} +{%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_customer_hashed')] -%} + +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} +``` - - ```(column, type, alias) ``` 3-tuples - - ```[column, type, alias] ``` 3-item lists - - ```'DOB'``` Single strings. +``` yaml tab="Union" -```yaml +hub_parts.sql: -{%- set tgt_pk = ['PART_PK', 'BINARY(16)', 'PART_PK'] -%} +{{- config(...) -}} -{{ dbtvault.cast([tgt_pk, - 'DOB', - ('PART_PK', 'NUMBER(38,0)', 'PART_ID'), - ('LOADDATE', 'DATE', 'LOADDATE'), - ('SOURCE', 'VARCHAR(15)', 'SOURCE')], - 'stg') }} +{%- set src_pk = ['PART_PK', 'PART_PK', 'PART_PK'] -%} +{%- set src_nk = ['PART_ID', 'PART_ID', 'PART_ID'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = [src_pk[0], src_nk[0], src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} +{%- set tgt_nk = [src_nk[0], 'NUMBER(38,0)', src_nk[0]] -%} +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_parts_hashed'), + ref('stg_supplier_hashed'), + ref('stg_lineitem_hashed')] -%} + + +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} ``` + #### Output -```mysql -CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, -stg.DOB, -CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, -CAST(stg.LOADDATE AS DATE) AS LOADDATE, -CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +```mysql tab="Single-Source" +SELECT DISTINCT + CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, + CAST(stg.CUSTOMER_ID AS VARCHAR(38)) AS CUSTOMER_ID, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT a.CUSTOMER_PK, a.CUSTOMER_ID, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_customer_hashed AS a +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.hub_customer AS tgt +ON stg.CUSTOMER_PK = tgt.CUSTOMER_PK +WHERE tgt.CUSTOMER_PK IS NULL +``` + +```mysql tab="Union" +SELECT DISTINCT + CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, + CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT PART_PK, PART_ID, LOADDATE, SOURCE, + LAG(SOURCE, 1) + OVER(PARTITION by PART_PK + ORDER BY PART_PK) AS FIRST_SOURCE + FROM ( + SELECT a.PART_PK, a.PART_ID, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_parts_hashed AS a + UNION + SELECT b.PART_PK, b.PART_ID, b.LOADDATE, b.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_supplier_hashed AS b + UNION + SELECT c.PART_PK, c.PART_ID, c.LOADDATE, c.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_lineitem_hashed AS c) +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.hub_parts AS tgt +ON stg.PART_PK = tgt.PART_PK +WHERE tgt.PART_PK IS NULL +AND stg.FIRST_SOURCE IS NULL ``` ___ -### md5_binary +### link_template -!!! warning - This macro ***should not be*** used for any kind of password obfuscation or security purposes, the intended use is for creating checksum-like fields only. - - [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) -A macro for generating hashing SQL for columns: -```sql -CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias -``` +Creates a link with provided metadata. -- Can provide multiple columns as a list to create a concatenated hash -- Casts a column as ```VARCHAR```, transforms to ```UPPER``` case and trims whitespace -- ```'^^'``` Accounts for null values with a double caret -- ```'||'``` Concatenates with a double pipe +```mysql +dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) +``` #### Parameters -| Parameter | Description | Required? | -| ---------------- | ---------------------------------------------- | -------------------------------------------------------- | -| columns | Single column string or list of columns | check_circle | -| alias | A string | check_circle | +| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | +| ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | List | check_circle | +| src_fk | Source foreign key column | List | List | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| tgt_cols | Complete list of all source columns (pre-aliasing) | List | List | check_circle | +| tgt_pk | Target primary key column | List | List | check_circle | +| tgt_fk | Target foreign key column | List | List | check_circle | +| tgt_ldts | Target loaddate timestamp column | List | List | check_circle | +| tgt_source | Name of the column which will contain the source ID | List | List | check_circle | +| source | Staging model reference or table name | List | List | check_circle | #### Usage -```yaml -{{ dbtvault.md5_binary('CUSTOMERKEY', 'CUSTOMER_PK') }}, -{{ dbtvault.md5_binary(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF') }} +``` yaml tab="Single-Source" + +link_customer_nation.sql: + +{{- config(...) -}} + +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', + src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_crm_customer_hashed')] -%} + +{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} +``` + +``` yaml tab="Union" + +link_customer_nation_union.sql: + +{{- config(...) -}} + +{%- set src_pk = ['CUSTOMER_NATION_PK', 'CUSTOMER_NATION_PK', 'CUSTOMER_NATION_PK'] -%} + +{%- set src_fk = [['CUSTOMER_PK', 'NATION_PK'], ['CUSTOMER_PK', 'NATION_PK'], + ['CUSTOMER_PK', 'NATION_PK']] -%} + +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', + src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_sap_customer_hashed'), + ref('stg_crm_customer_hashed'), + ref('stg_web_customer_hashed')] -%} + +{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} ``` -!!! tip - [gen_hashing](#gen_hashing) may be used to simplify the hashing process and generate multiple hashes with one macro. #### Output -```mysql -CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, -CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) - AS BINARY(16)) AS HASHDIFF +```mysql tab="Single-Source" +SELECT DISTINCT + CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, + CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, + CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS a +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation AS tgt +ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK +WHERE tgt.CUSTOMER_NATION_PK IS NULL +``` + +```mysql tab="Union" +SELECT DISTINCT + CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, + CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, + CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT CUSTOMER_NATION_PK, CUSTOMER_PK, NATION_PK, LOADDATE, SOURCE, + LAG(SOURCE, 1) + OVER(PARTITION by CUSTOMER_NATION_PK + ORDER BY CUSTOMER_NATION_PK) AS FIRST_SOURCE + FROM ( + SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_sap_customer_hashed AS a + UNION + SELECT b.CUSTOMER_NATION_PK, b.CUSTOMER_PK, b.NATION_PK, b.LOADDATE, b.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS b + UNION + SELECT c.CUSTOMER_NATION_PK, c.CUSTOMER_PK, c.NATION_PK, c.LOADDATE, c.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_web_customer_hashed AS c) +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation_union AS tgt +ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK +WHERE tgt.CUSTOMER_NATION_PK IS NULL +AND stg.FIRST_SOURCE IS NULL ``` ___ -### prefix +### sat_template -A macro for quickly prefixing a list of columns with a string: -```mysql -a.column1, a.column2, a.column3, a.column4 +Creates a satellite with provided metadata. + +```mysql +dbtvault.sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source, + src_table, source) ``` #### Parameters -| Parameter | Description | Required? | -| ---------------- | ----------------------------- | -------------------------------------------------------- | -| columns | A list of column names | check_circle | -| prefix_str | A string | check_circle | +| Parameter | Description | Type | Required? | +| ------------- | --------------------------------------------------- | ---------------------| ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | check_circle | +| src_hashdiff | Source hashdiff column | String | check_circle | +| src_payload | Source payload column(s) | List | check_circle | +| src_eff | Source effective from column | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| tgt_cols | Complete list of all source columns (pre-aliasing) | List | check_circle | +| tgt_pk | Target primary key column | List | check_circle | +| tgt_hashdiff | Target hashdiff column | List | check_circle | +| tgt_payload | Target payload column | List | check_circle | +| tgt_eff | Target effective from column | List | check_circle | +| tgt_ldts | Target loaddate timestamp column | List | check_circle | +| tgt_source | Name of the column which will contain the source ID | List | check_circle | +| source | Staging model reference or table name | List | check_circle | #### Usage -```yaml -{{ dbtvault.prefix(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'a') }} -{{ dbtvault.prefix(['CUSTOMERKEY'], 'a') + +``` yaml + +sat_customer_details.sql: + +{{- config(...) -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} +{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} + +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = [src_pk, 'HASHDIFF', 'NAME', 'DOB', 'PHONE', 'EFFECTIVE_FROM', 'LOADDATE', 'SOURCE'] -%} + +{%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} + +{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} + +{%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], + [ src_payload[1], 'DATE', 'DOB'], + [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} + +{%- set tgt_eff = ['EFFECTIVE_FROM', 'DATE', 'EFFECTIVE_FROM'] -%} +{%- set tgt_ldts = ['LOADDATE', 'DATE', 'LOADDATE'] -%} +{%- set tgt_source = ['SOURCE', 'VARCHAR(15)', 'SOURCE'] -%} + +{%- set source = [ref('stg_customer_details_hashed')] -%} + +{{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source, + source) }} ``` -!!! Note - Single columns must be provided as a 1-item list, as in the second example above. #### Output -```mysql -a.CUSTOMERKEY, a.DOB, a.NAME, a.PHONE -a.CUSTOMERKEY +```mysql +SELECT DISTINCT + CAST(e.CUSTOMER_HASHDIFF AS BINARY(16)) AS HASHDIFF, + CAST(e.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, + CAST(e.CUSTOMER_NAME AS VARCHAR(60)) AS NAME, + CAST(e.CUSTOMER_DOB AS DATE) AS DOB, + CAST(e.CUSTOMER_PHONE AS VARCHAR(15)) AS PHONE, + CAST(e.LOADDATE AS DATE) AS LOADDATE, + CAST(e.EFFECTIVE_FROM AS DATE) AS EFFECTIVE_FROM, + CAST(e.SOURCE AS VARCHAR(15)) AS SOURCE +FROM MYDATABASE.MYSCHEMA.stg_customer_details_hashed AS e +LEFT JOIN ( + SELECT d.CUSTOMER_PK, d.HASHDIFF, d.NAME, d.DOB, d.PHONE, d.EFFECTIVE_FROM, d.LOADDATE, d.SOURCE + FROM ( + SELECT c.CUSTOMER_PK, c.HASHDIFF, c.NAME, c.DOB, c.PHONE, c.EFFECTIVE_FROM, c.LOADDATE, c.SOURCE, + CASE WHEN RANK() + OVER (PARTITION BY c.CUSTOMER_PK + ORDER BY c.LOADDATE DESC) = 1 + THEN 'Y' ELSE 'N' END CURR_FLG + FROM ( + SELECT a.CUSTOMER_PK, a.HASHDIFF, a.NAME, a.DOB, a.PHONE, a.EFFECTIVE_FROM, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.sat_customer_details as a + JOIN MYDATABASE.MYSCHEMA.stg_customer_details_hashed as b + ON a.CUSTOMER_PK = b.CUSTOMER_PK + ) as c + ) AS d +WHERE d.CURR_FLG = 'Y') AS src +ON src.HASHDIFF = e.CUSTOMER_HASHDIFF +WHERE src.HASHDIFF IS NULL ``` -## Table templates \ No newline at end of file +___ + +## Internal + +Internal macros support the other macros provided in this package. +They are used to process provided metadata and should not be called directly. \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index efab1190b..2f2e07978 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ mkdocs-material==4.4.2 mkdocs-minify-plugin==0.2.1 -pygments \ No newline at end of file +pygments +pymdown-extensions \ No newline at end of file diff --git a/macros/internal/add_columns.sql b/macros/staging/add_columns.sql similarity index 100% rename from macros/internal/add_columns.sql rename to macros/staging/add_columns.sql diff --git a/macros/internal/gen_hashing.sql b/macros/staging/gen_hashing.sql similarity index 100% rename from macros/internal/gen_hashing.sql rename to macros/staging/gen_hashing.sql diff --git a/macros/internal/staging_footer.sql b/macros/staging/staging_footer.sql similarity index 100% rename from macros/internal/staging_footer.sql rename to macros/staging/staging_footer.sql diff --git a/macros/utility/cast.sql b/macros/supporting/cast.sql similarity index 100% rename from macros/utility/cast.sql rename to macros/supporting/cast.sql diff --git a/macros/utility/md5_binary.sql b/macros/supporting/md5_binary.sql similarity index 100% rename from macros/utility/md5_binary.sql rename to macros/supporting/md5_binary.sql diff --git a/macros/utility/prefix.sql b/macros/supporting/prefix.sql similarity index 100% rename from macros/utility/prefix.sql rename to macros/supporting/prefix.sql diff --git a/mkdocs.yml b/mkdocs.yml index 128cd0eab..b1a3fb8ce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,9 @@ site_name: dbtvault +site_author: Datavault theme: name: 'material' custom_dir: 'theme' + show_sidebar: true logo: 'assets/images/logo.png' banner: 'assets/images/docs-banner.png' favicon: 'assets/images/favicon.ico' @@ -16,11 +18,12 @@ repo_url: 'https://github.com/Datavault-UK/dbtvault' nav: - Home: 'index.md' - Getting Started: 'gettingstarted.md' - - Macros: 'macros.md' - Table types: - Hubs: 'table-types/hubs.md' - Links: 'table-types/links.md' - Satellites: 'table-types/satellites.md' + - Macros: 'macros.md' + - Licence: 'LICENSE.md' extra: social: @@ -38,6 +41,11 @@ extra: markdown_extensions: - codehilite - admonition + - pymdownx.superfences + - toc: + permalink: true extra_css: - - 'stylesheets/cube.css' \ No newline at end of file + - 'stylesheets/cube.css' + +copyright: Datavault © 2019 \ No newline at end of file diff --git a/theme/main.html b/theme/main.html index 2f1f1f4b8..333faee70 100644 --- a/theme/main.html +++ b/theme/main.html @@ -20,4 +20,17 @@ {% endif %} + + +{% if page.toc %} +
+
+
+ {% include "partials/toc.html" %} +
+
+
+{% endif %} + {% endblock %} \ No newline at end of file From 7abf22e229a08eb5cba05428c68f914d18855a4b Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 16:53:13 +0100 Subject: [PATCH 021/157] Added dbt trademark information --- README.md | 5 ++++- mkdocs.yml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f3593c769..078bdecfc 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,10 @@ # dbtvault by [Datavault](https://www.data-vault.co.uk) -dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses. +dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses; + +powered by [dbt](https://www.getdbt.com/), a registered trademark of [Fishtown Analytics](https://www.fishtownanalytics.com/) + ## Currently supported databases: diff --git a/mkdocs.yml b/mkdocs.yml index b1a3fb8ce..38621eee5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,4 +48,4 @@ markdown_extensions: extra_css: - 'stylesheets/cube.css' -copyright: Datavault © 2019 \ No newline at end of file +copyright: dbtvault and docs © Datavault 2019 | dbt is a registered trademark of Fishtown Analytics \ No newline at end of file From 5e86719cb7834a11b249bfe5dbcd4cdc36a6e2dd Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 17:03:45 +0100 Subject: [PATCH 022/157] Added 'Read more' back --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 078bdecfc..621105dcd 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ packages: And run ```dbt deps``` +[Read more on package installation](https://docs.getdbt.com/docs/package-management) + ## Usage 1. Create a model for your hub, link or satellite From 80802af92a9d450ed226e8312f9e2a5cf9283a34 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 17:30:32 +0100 Subject: [PATCH 023/157] Reduced toc depth --- mkdocs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 38621eee5..de62dafb3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,8 +44,9 @@ markdown_extensions: - pymdownx.superfences - toc: permalink: true + toc_depth: 1-3 extra_css: - 'stylesheets/cube.css' -copyright: dbtvault and docs © Datavault 2019 | dbt is a registered trademark of Fishtown Analytics \ No newline at end of file +copyright: Datavault 2019 \ No newline at end of file From b9cce45d170a0d06d43423702ef94548286ae507 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 17:32:24 +0100 Subject: [PATCH 024/157] Added back copyright Accidentally overwrote with an old file --- mkdocs.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index de62dafb3..38621eee5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,9 +44,8 @@ markdown_extensions: - pymdownx.superfences - toc: permalink: true - toc_depth: 1-3 extra_css: - 'stylesheets/cube.css' -copyright: Datavault 2019 \ No newline at end of file +copyright: dbtvault and docs © Datavault 2019 | dbt is a registered trademark of Fishtown Analytics \ No newline at end of file From cd0f334fa5f051a7626f6d30a95713c94b158f38 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 17:33:33 +0100 Subject: [PATCH 025/157] Reduced toc depth --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 38621eee5..d6799052d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,6 +44,7 @@ markdown_extensions: - pymdownx.superfences - toc: permalink: true + toc-depth: 1-3 extra_css: - 'stylesheets/cube.css' From b7b0748ef75a7489ca2388dca7f406be300dcf8c Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 17:38:00 +0100 Subject: [PATCH 026/157] Fixed docs --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index d6799052d..d20982669 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,7 +44,7 @@ markdown_extensions: - pymdownx.superfences - toc: permalink: true - toc-depth: 1-3 + toc_depth: 1-3 extra_css: - 'stylesheets/cube.css' From 0b17dedcbd98d0caf9bf04961e8d5f35133b4e4e Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 17:52:24 +0100 Subject: [PATCH 027/157] Re-organised folder layout - Changed docs accordingly --- docs/macros.md | 878 +++++++++++++------------- macros/{ => tables}/hub_template.sql | 0 macros/{ => tables}/link_template.sql | 0 macros/{ => tables}/sat_template.sql | 0 4 files changed, 441 insertions(+), 437 deletions(-) rename macros/{ => tables}/hub_template.sql (100%) rename macros/{ => tables}/link_template.sql (100%) rename macros/{ => tables}/sat_template.sql (100%) diff --git a/docs/macros.md b/docs/macros.md index 36f115b31..6f3f7468d 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -1,177 +1,417 @@ -## Support Macros - -Support macros are helper functions for use in models. It should not be necessary to call these macros directly, however they -are used extensively in the [table templates](#table-templates). +## Table templates +######(macros/tables) +These macros form the core of the package and can be called in your models to build the tables for your Data Vault. ___ -### cast +### hub_template -A macro for generating cast sequences: +Creates a hub with provided metadata. -```mysql -CAST(prefix.column AS type) AS alias +```mysql +dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) ``` #### Parameters -| Parameter | Description | Required? | -| ---------------- | ----------------------------- | -------------------------------------------------------- | -| columns | Triples or strings | check_circle | -| prefix | A string | clear | +| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | +| ------------- | --------------------------------------------------- | -------------------- | -------------------- | ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | List | check_circle | +| src_nk | Source natural key column | String | List | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| tgt_cols | Complete list of all source columns (pre-aliasing) | List | List | check_circle | +| tgt_pk | Target primary key column | List | List | check_circle | +| tgt_nk | Target natural key column | List | List | check_circle | +| tgt_ldts | Target loaddate timestamp column | List | List | check_circle | +| tgt_source | Name of the column which will contain the source ID | List | List | check_circle | +| source | Staging model reference or table name | List | List | check_circle | #### Usage -!!! note - As shown in the snippet below, columns must be provided as a list. - The collection of items in this list can be any combination of: - - - ```(column, type, alias) ``` 3-tuples - - ```[column, type, alias] ``` 3-item lists - - ```'DOB'``` Single strings. - -```yaml +``` yaml tab="Single-Source" -{%- set tgt_pk = ['PART_PK', 'BINARY(16)', 'PART_PK'] -%} +hub_customer.sql: -{{ dbtvault.cast([tgt_pk, - 'DOB', - ('PART_PK', 'NUMBER(38,0)', 'PART_ID'), - ('LOADDATE', 'DATE', 'LOADDATE'), - ('SOURCE', 'VARCHAR(15)', 'SOURCE')], - 'stg') }} +{{- config(...) -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_nk = 'CUSTOMER_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = [src_pk, src_nk, src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} +{%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_customer_hashed')] -%} + +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} ``` -#### Output - -```mysql -CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, -stg.DOB, -CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, -CAST(stg.LOADDATE AS DATE) AS LOADDATE, -CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE -``` +``` yaml tab="Union" -___ +hub_parts.sql: -### md5_binary +{{- config(...) -}} -!!! warning - This macro ***should not be*** used for cryptographic purposes - - The intended use is for creating checksum-like fields only, so that a record change can be detected. - - [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) - -A macro for generating hashing SQL for columns: -```sql -CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias -``` +{%- set src_pk = ['PART_PK', 'PART_PK', 'PART_PK'] -%} +{%- set src_nk = ['PART_ID', 'PART_ID', 'PART_ID'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} -- Can provide multiple columns as a list to create a concatenated hash -- Casts a column as ```VARCHAR```, transforms to ```UPPER``` case and trims whitespace -- ```'^^'``` Accounts for null values with a double caret -- ```'||'``` Concatenates with a double pipe +{%- set tgt_cols = [src_pk[0], src_nk[0], src_ldts, src_source] -%} -#### Parameters +{%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} +{%- set tgt_nk = [src_nk[0], 'NUMBER(38,0)', src_nk[0]] -%} +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} -| Parameter | Description | Type | Required? | -| ---------------- | ---------------------------------------------- | ----------- | -------------------------------------------------------- | -| columns | Columns to hash on | String/List | check_circle | -| alias | The name to give the hashed column | String | check_circle | +{%- set source = [ref('stg_parts_hashed'), + ref('stg_supplier_hashed'), + ref('stg_lineitem_hashed')] -%} -#### Usage -```yaml -{{ dbtvault.md5_binary('CUSTOMERKEY', 'CUSTOMER_PK') }}, -{{ dbtvault.md5_binary(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF') }} +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} ``` -!!! tip - [gen_hashing](#gen_hashing) may be used to simplify the hashing process and generate multiple hashes with one macro. #### Output -```mysql -CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, -CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) - AS BINARY(16)) AS HASHDIFF +```mysql tab="Single-Source" +SELECT DISTINCT + CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, + CAST(stg.CUSTOMER_ID AS VARCHAR(38)) AS CUSTOMER_ID, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT a.CUSTOMER_PK, a.CUSTOMER_ID, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_customer_hashed AS a +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.hub_customer AS tgt +ON stg.CUSTOMER_PK = tgt.CUSTOMER_PK +WHERE tgt.CUSTOMER_PK IS NULL +``` + +```mysql tab="Union" +SELECT DISTINCT + CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, + CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT PART_PK, PART_ID, LOADDATE, SOURCE, + LAG(SOURCE, 1) + OVER(PARTITION by PART_PK + ORDER BY PART_PK) AS FIRST_SOURCE + FROM ( + SELECT a.PART_PK, a.PART_ID, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_parts_hashed AS a + UNION + SELECT b.PART_PK, b.PART_ID, b.LOADDATE, b.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_supplier_hashed AS b + UNION + SELECT c.PART_PK, c.PART_ID, c.LOADDATE, c.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_lineitem_hashed AS c) +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.hub_parts AS tgt +ON stg.PART_PK = tgt.PART_PK +WHERE tgt.PART_PK IS NULL +AND stg.FIRST_SOURCE IS NULL ``` ___ -### prefix +### link_template -A macro for quickly prefixing a list of columns with a string: -```mysql -a.column1, a.column2, a.column3, a.column4 +Creates a link with provided metadata. + +```mysql +dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) ``` #### Parameters -| Parameter | Description | Type | Required? | -| ---------------- | ----------------------------- | ------ | -------------------------------------------------------- | -| columns | A list of column names | List | check_circle | -| prefix_str | The prefix for the columns | String | check_circle | +| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | +| ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | List | check_circle | +| src_fk | Source foreign key column | List | List | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| tgt_cols | Complete list of all source columns (pre-aliasing) | List | List | check_circle | +| tgt_pk | Target primary key column | List | List | check_circle | +| tgt_fk | Target foreign key column | List | List | check_circle | +| tgt_ldts | Target loaddate timestamp column | List | List | check_circle | +| tgt_source | Name of the column which will contain the source ID | List | List | check_circle | +| source | Staging model reference or table name | List | List | check_circle | #### Usage -```yaml -{{ dbtvault.prefix(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'a') }} -{{ dbtvault.prefix(['CUSTOMERKEY'], 'a') -``` +``` yaml tab="Single-Source" -!!! Note - Single columns must be provided as a 1-item list, as in the second example above. +link_customer_nation.sql: -#### Output +{{- config(...) -}} -```mysql -a.CUSTOMERKEY, a.DOB, a.NAME, a.PHONE -a.CUSTOMERKEY -``` +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} -___ +{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', + src_ldts, src_source] -%} -## Staging Macros +{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} -These macros are intended for use in the staging layer -___ +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} -### gen_hashing +{%- set source = [ref('stg_crm_customer_hashed')] -%} -!!! warning - This macro ***should not be*** used for cryptographic purposes. - - The intended use is for creating checksum-like fields only, so that a record change can be detected. - - [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) - -!!! seealso - [md5_binary](#md5_binary) - -A macro for generating multiple lines of hashing SQL for columns: -```sql -CAST(MD5_BINARY(UPPER(TRIM(CAST(column1 AS VARCHAR)))) AS BINARY(16)) AS alias1, -CAST(MD5_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(16)) AS alias2 +{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} ``` -#### Parameters +``` yaml tab="Union" -| Parameter | Description | Type | Required? | -| ---------------- | ---------------------------------------------- | ------ | -------------------------------------------------------- | -| pairs | (column, alias) pair | Tuple | check_circle | -| pairs: columns | Single column string or list of columns | String | check_circle | -| pairs: alias | The alias for the column | String | check_circle | +link_customer_nation_union.sql: +{{- config(...) -}} -#### Usage +{%- set src_pk = ['CUSTOMER_NATION_PK', 'CUSTOMER_NATION_PK', 'CUSTOMER_NATION_PK'] -%} -```yaml -{{ dbtvault.gen_hashing([('CUSTOMERKEY', 'CUSTOMER_PK'), +{%- set src_fk = [['CUSTOMER_PK', 'NATION_PK'], ['CUSTOMER_PK', 'NATION_PK'], + ['CUSTOMER_PK', 'NATION_PK']] -%} + +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', + src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_sap_customer_hashed'), + ref('stg_crm_customer_hashed'), + ref('stg_web_customer_hashed')] -%} + +{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} +``` + + +#### Output + +```mysql tab="Single-Source" +SELECT DISTINCT + CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, + CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, + CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS a +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation AS tgt +ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK +WHERE tgt.CUSTOMER_NATION_PK IS NULL +``` + +```mysql tab="Union" +SELECT DISTINCT + CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, + CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, + CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT CUSTOMER_NATION_PK, CUSTOMER_PK, NATION_PK, LOADDATE, SOURCE, + LAG(SOURCE, 1) + OVER(PARTITION by CUSTOMER_NATION_PK + ORDER BY CUSTOMER_NATION_PK) AS FIRST_SOURCE + FROM ( + SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_sap_customer_hashed AS a + UNION + SELECT b.CUSTOMER_NATION_PK, b.CUSTOMER_PK, b.NATION_PK, b.LOADDATE, b.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS b + UNION + SELECT c.CUSTOMER_NATION_PK, c.CUSTOMER_PK, c.NATION_PK, c.LOADDATE, c.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_web_customer_hashed AS c) +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation_union AS tgt +ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK +WHERE tgt.CUSTOMER_NATION_PK IS NULL +AND stg.FIRST_SOURCE IS NULL +``` + +___ + +### sat_template + +Creates a satellite with provided metadata. + +```mysql +dbtvault.sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source, + src_table, source) +``` + +#### Parameters + +| Parameter | Description | Type | Required? | +| ------------- | --------------------------------------------------- | ---------------------| ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | check_circle | +| src_hashdiff | Source hashdiff column | String | check_circle | +| src_payload | Source payload column(s) | List | check_circle | +| src_eff | Source effective from column | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| tgt_cols | Complete list of all source columns (pre-aliasing) | List | check_circle | +| tgt_pk | Target primary key column | List | check_circle | +| tgt_hashdiff | Target hashdiff column | List | check_circle | +| tgt_payload | Target payload column | List | check_circle | +| tgt_eff | Target effective from column | List | check_circle | +| tgt_ldts | Target loaddate timestamp column | List | check_circle | +| tgt_source | Name of the column which will contain the source ID | List | check_circle | +| source | Staging model reference or table name | List | check_circle | + +#### Usage + + +``` yaml + +sat_customer_details.sql: + +{{- config(...) -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} +{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} + +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = [src_pk, 'HASHDIFF', 'NAME', 'DOB', 'PHONE', 'EFFECTIVE_FROM', 'LOADDATE', 'SOURCE'] -%} + +{%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} + +{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} + +{%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], + [ src_payload[1], 'DATE', 'DOB'], + [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} + +{%- set tgt_eff = ['EFFECTIVE_FROM', 'DATE', 'EFFECTIVE_FROM'] -%} +{%- set tgt_ldts = ['LOADDATE', 'DATE', 'LOADDATE'] -%} +{%- set tgt_source = ['SOURCE', 'VARCHAR(15)', 'SOURCE'] -%} + +{%- set source = [ref('stg_customer_details_hashed')] -%} + +{{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source, + source) }} +``` + + +#### Output + +```mysql +SELECT DISTINCT + CAST(e.CUSTOMER_HASHDIFF AS BINARY(16)) AS HASHDIFF, + CAST(e.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, + CAST(e.CUSTOMER_NAME AS VARCHAR(60)) AS NAME, + CAST(e.CUSTOMER_DOB AS DATE) AS DOB, + CAST(e.CUSTOMER_PHONE AS VARCHAR(15)) AS PHONE, + CAST(e.LOADDATE AS DATE) AS LOADDATE, + CAST(e.EFFECTIVE_FROM AS DATE) AS EFFECTIVE_FROM, + CAST(e.SOURCE AS VARCHAR(15)) AS SOURCE +FROM MYDATABASE.MYSCHEMA.stg_customer_details_hashed AS e +LEFT JOIN ( + SELECT d.CUSTOMER_PK, d.HASHDIFF, d.NAME, d.DOB, d.PHONE, d.EFFECTIVE_FROM, d.LOADDATE, d.SOURCE + FROM ( + SELECT c.CUSTOMER_PK, c.HASHDIFF, c.NAME, c.DOB, c.PHONE, c.EFFECTIVE_FROM, c.LOADDATE, c.SOURCE, + CASE WHEN RANK() + OVER (PARTITION BY c.CUSTOMER_PK + ORDER BY c.LOADDATE DESC) = 1 + THEN 'Y' ELSE 'N' END CURR_FLG + FROM ( + SELECT a.CUSTOMER_PK, a.HASHDIFF, a.NAME, a.DOB, a.PHONE, a.EFFECTIVE_FROM, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.sat_customer_details as a + JOIN MYDATABASE.MYSCHEMA.stg_customer_details_hashed as b + ON a.CUSTOMER_PK = b.CUSTOMER_PK + ) as c + ) AS d +WHERE d.CURR_FLG = 'Y') AS src +ON src.HASHDIFF = e.CUSTOMER_HASHDIFF +WHERE src.HASHDIFF IS NULL +``` + +___ + +## Staging Macros +######(macros/staging) + +These macros are intended for use in the staging layer +___ + +### gen_hashing + +!!! warning + This macro ***should not be*** used for cryptographic purposes. + + The intended use is for creating checksum-like fields only, so that a record change can be detected. + + [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) + +!!! seealso + [md5_binary](#md5_binary) + +A macro for generating multiple lines of hashing SQL for columns: +```sql +CAST(MD5_BINARY(UPPER(TRIM(CAST(column1 AS VARCHAR)))) AS BINARY(16)) AS alias1, +CAST(MD5_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(16)) AS alias2 +``` + +#### Parameters + +| Parameter | Description | Type | Required? | +| ---------------- | ---------------------------------------------- | ------ | -------------------------------------------------------- | +| pairs | (column, alias) pair | Tuple | check_circle | +| pairs: columns | Single column string or list of columns | String | check_circle | +| pairs: alias | The alias for the column | String | check_circle | + + +#### Usage + +```yaml +{{ dbtvault.gen_hashing([('CUSTOMERKEY', 'CUSTOMER_PK'), (['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF')]) }} ``` @@ -259,383 +499,147 @@ Used in creating source/hashing models to complete a staging layer model. ___ -## Table templates +## Supporting Macros +######(macros/supporting) + +Supporting macros are helper functions for use in models. It should not be necessary to call these macros directly, however they +are used extensively in the [table templates](#table-templates). -These macros form the core of the package and can be called in your models to build the tables for your Data Vault. ___ -### hub_template +### cast -Creates a hub with provided metadata. +A macro for generating cast sequences: -```mysql -dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) +```mysql +CAST(prefix.column AS type) AS alias ``` #### Parameters -| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | -| ------------- | --------------------------------------------------- | -------------------- | -------------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | List | check_circle | -| src_nk | Source natural key column | String | List | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_cols | Complete list of all source columns (pre-aliasing) | List | List | check_circle | -| tgt_pk | Target primary key column | List | List | check_circle | -| tgt_nk | Target natural key column | List | List | check_circle | -| tgt_ldts | Target loaddate timestamp column | List | List | check_circle | -| tgt_source | Name of the column which will contain the source ID | List | List | check_circle | -| source | Staging model reference or table name | List | List | check_circle | +| Parameter | Description | Required? | +| ---------------- | ----------------------------- | -------------------------------------------------------- | +| columns | Triples or strings | check_circle | +| prefix | A string | clear | #### Usage -``` yaml tab="Single-Source" +!!! note + As shown in the snippet below, columns must be provided as a list. + The collection of items in this list can be any combination of: -hub_customer.sql: + - ```(column, type, alias) ``` 3-tuples + - ```[column, type, alias] ``` 3-item lists + - ```'DOB'``` Single strings. -{{- config(...) -}} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_nk = 'CUSTOMER_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_cols = [src_pk, src_nk, src_ldts, src_source] -%} - -{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} -{%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} -{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} -{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} -``` - -``` yaml tab="Union" - -hub_parts.sql: - -{{- config(...) -}} - -{%- set src_pk = ['PART_PK', 'PART_PK', 'PART_PK'] -%} -{%- set src_nk = ['PART_ID', 'PART_ID', 'PART_ID'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_cols = [src_pk[0], src_nk[0], src_ldts, src_source] -%} - -{%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} -{%- set tgt_nk = [src_nk[0], 'NUMBER(38,0)', src_nk[0]] -%} -{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} -{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} - -{%- set source = [ref('stg_parts_hashed'), - ref('stg_supplier_hashed'), - ref('stg_lineitem_hashed')] -%} +```yaml +{%- set tgt_pk = ['PART_PK', 'BINARY(16)', 'PART_PK'] -%} -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} +{{ dbtvault.cast([tgt_pk, + 'DOB', + ('PART_PK', 'NUMBER(38,0)', 'PART_ID'), + ('LOADDATE', 'DATE', 'LOADDATE'), + ('SOURCE', 'VARCHAR(15)', 'SOURCE')], + 'stg') }} ``` - #### Output -```mysql tab="Single-Source" -SELECT DISTINCT - CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, - CAST(stg.CUSTOMER_ID AS VARCHAR(38)) AS CUSTOMER_ID, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE -FROM ( - SELECT a.CUSTOMER_PK, a.CUSTOMER_ID, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_customer_hashed AS a -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.hub_customer AS tgt -ON stg.CUSTOMER_PK = tgt.CUSTOMER_PK -WHERE tgt.CUSTOMER_PK IS NULL -``` - -```mysql tab="Union" -SELECT DISTINCT - CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, - CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE -FROM ( - SELECT PART_PK, PART_ID, LOADDATE, SOURCE, - LAG(SOURCE, 1) - OVER(PARTITION by PART_PK - ORDER BY PART_PK) AS FIRST_SOURCE - FROM ( - SELECT a.PART_PK, a.PART_ID, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_parts_hashed AS a - UNION - SELECT b.PART_PK, b.PART_ID, b.LOADDATE, b.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_supplier_hashed AS b - UNION - SELECT c.PART_PK, c.PART_ID, c.LOADDATE, c.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_lineitem_hashed AS c) -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.hub_parts AS tgt -ON stg.PART_PK = tgt.PART_PK -WHERE tgt.PART_PK IS NULL -AND stg.FIRST_SOURCE IS NULL +```mysql +CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, +stg.DOB, +CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, +CAST(stg.LOADDATE AS DATE) AS LOADDATE, +CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE ``` ___ -### link_template - -Creates a link with provided metadata. +### md5_binary -```mysql -dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) +!!! warning + This macro ***should not be*** used for cryptographic purposes + + The intended use is for creating checksum-like fields only, so that a record change can be detected. + + [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) + +A macro for generating hashing SQL for columns: +```sql +CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias ``` +- Can provide multiple columns as a list to create a concatenated hash +- Casts a column as ```VARCHAR```, transforms to ```UPPER``` case and trims whitespace +- ```'^^'``` Accounts for null values with a double caret +- ```'||'``` Concatenates with a double pipe + #### Parameters -| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | -| ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | List | check_circle | -| src_fk | Source foreign key column | List | List | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_cols | Complete list of all source columns (pre-aliasing) | List | List | check_circle | -| tgt_pk | Target primary key column | List | List | check_circle | -| tgt_fk | Target foreign key column | List | List | check_circle | -| tgt_ldts | Target loaddate timestamp column | List | List | check_circle | -| tgt_source | Name of the column which will contain the source ID | List | List | check_circle | -| source | Staging model reference or table name | List | List | check_circle | +| Parameter | Description | Type | Required? | +| ---------------- | ---------------------------------------------- | ----------- | -------------------------------------------------------- | +| columns | Columns to hash on | String/List | check_circle | +| alias | The name to give the hashed column | String | check_circle | #### Usage -``` yaml tab="Single-Source" - -link_customer_nation.sql: - -{{- config(...) -}} - -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', - src_ldts, src_source] -%} - -{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} - -{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} -{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} - -{%- set source = [ref('stg_crm_customer_hashed')] -%} - -{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} -``` - -``` yaml tab="Union" - -link_customer_nation_union.sql: - -{{- config(...) -}} - -{%- set src_pk = ['CUSTOMER_NATION_PK', 'CUSTOMER_NATION_PK', 'CUSTOMER_NATION_PK'] -%} - -{%- set src_fk = [['CUSTOMER_PK', 'NATION_PK'], ['CUSTOMER_PK', 'NATION_PK'], - ['CUSTOMER_PK', 'NATION_PK']] -%} - -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', - src_ldts, src_source] -%} - -{%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} - -{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} -{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} - -{%- set source = [ref('stg_sap_customer_hashed'), - ref('stg_crm_customer_hashed'), - ref('stg_web_customer_hashed')] -%} - -{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} +```yaml +{{ dbtvault.md5_binary('CUSTOMERKEY', 'CUSTOMER_PK') }}, +{{ dbtvault.md5_binary(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF') }} ``` +!!! tip + [gen_hashing](#gen_hashing) may be used to simplify the hashing process and generate multiple hashes with one macro. #### Output -```mysql tab="Single-Source" -SELECT DISTINCT - CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, - CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, - CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE -FROM ( - SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS a -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation AS tgt -ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK -WHERE tgt.CUSTOMER_NATION_PK IS NULL -``` - -```mysql tab="Union" -SELECT DISTINCT - CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, - CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, - CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE -FROM ( - SELECT CUSTOMER_NATION_PK, CUSTOMER_PK, NATION_PK, LOADDATE, SOURCE, - LAG(SOURCE, 1) - OVER(PARTITION by CUSTOMER_NATION_PK - ORDER BY CUSTOMER_NATION_PK) AS FIRST_SOURCE - FROM ( - SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_sap_customer_hashed AS a - UNION - SELECT b.CUSTOMER_NATION_PK, b.CUSTOMER_PK, b.NATION_PK, b.LOADDATE, b.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS b - UNION - SELECT c.CUSTOMER_NATION_PK, c.CUSTOMER_PK, c.NATION_PK, c.LOADDATE, c.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_web_customer_hashed AS c) -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation_union AS tgt -ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK -WHERE tgt.CUSTOMER_NATION_PK IS NULL -AND stg.FIRST_SOURCE IS NULL +```mysql +CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, +CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) + AS BINARY(16)) AS HASHDIFF ``` ___ -### sat_template - -Creates a satellite with provided metadata. +### prefix -```mysql -dbtvault.sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - src_table, source) +A macro for quickly prefixing a list of columns with a string: +```mysql +a.column1, a.column2, a.column3, a.column4 ``` #### Parameters -| Parameter | Description | Type | Required? | -| ------------- | --------------------------------------------------- | ---------------------| ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_hashdiff | Source hashdiff column | String | check_circle | -| src_payload | Source payload column(s) | List | check_circle | -| src_eff | Source effective from column | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| tgt_cols | Complete list of all source columns (pre-aliasing) | List | check_circle | -| tgt_pk | Target primary key column | List | check_circle | -| tgt_hashdiff | Target hashdiff column | List | check_circle | -| tgt_payload | Target payload column | List | check_circle | -| tgt_eff | Target effective from column | List | check_circle | -| tgt_ldts | Target loaddate timestamp column | List | check_circle | -| tgt_source | Name of the column which will contain the source ID | List | check_circle | -| source | Staging model reference or table name | List | check_circle | +| Parameter | Description | Type | Required? | +| ---------------- | ----------------------------- | ------ | -------------------------------------------------------- | +| columns | A list of column names | List | check_circle | +| prefix_str | The prefix for the columns | String | check_circle | #### Usage - -``` yaml - -sat_customer_details.sql: - -{{- config(...) -}} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} -{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} - -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_cols = [src_pk, 'HASHDIFF', 'NAME', 'DOB', 'PHONE', 'EFFECTIVE_FROM', 'LOADDATE', 'SOURCE'] -%} - -{%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} - -{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} - -{%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], - [ src_payload[1], 'DATE', 'DOB'], - [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} - -{%- set tgt_eff = ['EFFECTIVE_FROM', 'DATE', 'EFFECTIVE_FROM'] -%} -{%- set tgt_ldts = ['LOADDATE', 'DATE', 'LOADDATE'] -%} -{%- set tgt_source = ['SOURCE', 'VARCHAR(15)', 'SOURCE'] -%} - -{%- set source = [ref('stg_customer_details_hashed')] -%} - -{{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - source) }} +```yaml +{{ dbtvault.prefix(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'a') }} +{{ dbtvault.prefix(['CUSTOMERKEY'], 'a') ``` +!!! Note + Single columns must be provided as a 1-item list, as in the second example above. #### Output -```mysql -SELECT DISTINCT - CAST(e.CUSTOMER_HASHDIFF AS BINARY(16)) AS HASHDIFF, - CAST(e.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, - CAST(e.CUSTOMER_NAME AS VARCHAR(60)) AS NAME, - CAST(e.CUSTOMER_DOB AS DATE) AS DOB, - CAST(e.CUSTOMER_PHONE AS VARCHAR(15)) AS PHONE, - CAST(e.LOADDATE AS DATE) AS LOADDATE, - CAST(e.EFFECTIVE_FROM AS DATE) AS EFFECTIVE_FROM, - CAST(e.SOURCE AS VARCHAR(15)) AS SOURCE -FROM MYDATABASE.MYSCHEMA.stg_customer_details_hashed AS e -LEFT JOIN ( - SELECT d.CUSTOMER_PK, d.HASHDIFF, d.NAME, d.DOB, d.PHONE, d.EFFECTIVE_FROM, d.LOADDATE, d.SOURCE - FROM ( - SELECT c.CUSTOMER_PK, c.HASHDIFF, c.NAME, c.DOB, c.PHONE, c.EFFECTIVE_FROM, c.LOADDATE, c.SOURCE, - CASE WHEN RANK() - OVER (PARTITION BY c.CUSTOMER_PK - ORDER BY c.LOADDATE DESC) = 1 - THEN 'Y' ELSE 'N' END CURR_FLG - FROM ( - SELECT a.CUSTOMER_PK, a.HASHDIFF, a.NAME, a.DOB, a.PHONE, a.EFFECTIVE_FROM, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.sat_customer_details as a - JOIN MYDATABASE.MYSCHEMA.stg_customer_details_hashed as b - ON a.CUSTOMER_PK = b.CUSTOMER_PK - ) as c - ) AS d -WHERE d.CURR_FLG = 'Y') AS src -ON src.HASHDIFF = e.CUSTOMER_HASHDIFF -WHERE src.HASHDIFF IS NULL +```mysql +a.CUSTOMERKEY, a.DOB, a.NAME, a.PHONE +a.CUSTOMERKEY ``` ___ ## Internal +######(macros/internal) Internal macros support the other macros provided in this package. They are used to process provided metadata and should not be called directly. \ No newline at end of file diff --git a/macros/hub_template.sql b/macros/tables/hub_template.sql similarity index 100% rename from macros/hub_template.sql rename to macros/tables/hub_template.sql diff --git a/macros/link_template.sql b/macros/tables/link_template.sql similarity index 100% rename from macros/link_template.sql rename to macros/tables/link_template.sql diff --git a/macros/sat_template.sql b/macros/tables/sat_template.sql similarity index 100% rename from macros/sat_template.sql rename to macros/tables/sat_template.sql From a6e195bc8d19eda6621bf12255b0fbdc00f58443 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 18:09:35 +0100 Subject: [PATCH 028/157] Updated the homepage --- docs/index.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 017c48eac..eb41b3f60 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,66 @@ -# The dbt package for Data Vault 2.0 +# Welcome to dbtvault! +dbtvault is a dbt package that implements a Data Vault 2.0 Data Warehouse on a Snowflake database. -Full documentation coming soon, please provide feedback on our github in the meantime! +!!! Note + You need to be running dbt to use the package. + + [dbt](https://www.getdbt.com/) is a registered trademark of [Fishtown Analytics](https://www.fishtownanalytics.com/). + + Go check them out! -Thank you \ No newline at end of file +dbt is designed for ease of use in data engineering: for when you need to develop a data pipeline. It is a single executable that can run on your desktop or a VM in your network, it is developed in Python, and is free to download and use. + +## What is Data Vault 2.0? +Data Vault 2.0 is an Agile method used to deliver a highly scalable enterprise Data Warehouse. + +The method covers the full approach for developing a Data Warehouse: architecture, data modelling, the development approach, and includes a number of unique techniques. + +If you want to learn about the method, your best starting point is the book Building a Scalable Data Warehouse with Data Vault 2.0 (see details below). + +Data Vault 2.0 supports code automation. +Essentially the method uses a small set of standard building blocks to model your data warehouse (Hubs, Links and Satellites in the Raw Data Vault) and, because they are standardised, you can load these blocks with templated SQL. The result is a template-driven implementation, populated by metadata. You provide the metadata (table names and mapping details) and SQL is generated automatically. This leads to better quality code, fewer mistakes, and greatly improved productivity: i.e. Agility. + +## What does dbtvault do? +The dbtvault package generates and runs Data Vault ETL code from your metadata. + +Just like other dbt projects, you write a model for each ETL step. You provide the metadata for each model as declared variables and include code to invoke a macro from the dbtvault package. +The macro does the rest of the work – it processes the metadata, generates Snowflake SQL and executes the load respecting any and all dependencies. + +dbt even runs the load in parallel. As Data Vault 2.0 is designed for parallel load and Snowflake is highly performant, your ETL load will finish in rapid time. + +dbtvault reduces the need to write Snowflake SQL by hand to load the Data Vault. This is a repetitive, time-consuming and potentially error prone task. + + +## What features does dbt running the dbtvault package offer? +dbt works with the dbtvault package to: + +- Generate SQL to process the staging layer and load the data vault. +- Ensures consistency and correctness in the generated SQL. +- Identify dependencies between SQL statements. +- Create Raw Data Vault tables when a release first identifies them. +- Execute all generated SQL statements as a complete set. +- Execute data load in parallel up to a user-defined number of parallel threads. +- Generate data flow diagrams showing data lineage. + +## dbtvault versions and roadmap + +Version 1 of the dbtvault package provides a suite of macros that generate Snowflake SQL statements to pre-process staging layer tables and load hubs, links and satellite to a raw Data Vault. + +Later versions will extend the range of table types that the package handles. We are already working on Version 2 which will add support for non-historised links (also known as transactional links). + +## You Do Need Some Prior Knowledge About the Data Vault 2.0 Method +If you are going to use the dbtvault package for your Data Vault 2.0 project, then we expect you to have some prior knowledge about the Data Vault 2.0 method. + + +## How Can You Get Up to Speed on Data Vault 2.0? +You can get further information about the Data Vault 2.0 method from the following resources: + +### Books (from Amazon) + +- Building a Scalable Data Warehouse with Data Vault 2.0, Dan Linstedt and Michael Olschimke +- Better Data Modelling: An Introduction to Agile Data Engineering Using Data Vault 2.0, Kent Graziano + +### Blogs and Downloads + +- [What is Data Vault?](www.data-vault.co.uk/what-is-data-vault/) +- [Agile Modeling: Not an Option Anymore](https://www.vertabelo.com/blog/data-vault-series-agile-modeling-not-an-option-anymore/) From 6f852051a3edff3716c59b53392bfa4d689d6fff Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 30 Sep 2019 18:27:23 +0100 Subject: [PATCH 029/157] Added links to amazon books --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index eb41b3f60..495c71151 100644 --- a/docs/index.md +++ b/docs/index.md @@ -57,8 +57,8 @@ You can get further information about the Data Vault 2.0 method from the followi ### Books (from Amazon) -- Building a Scalable Data Warehouse with Data Vault 2.0, Dan Linstedt and Michael Olschimke -- Better Data Modelling: An Introduction to Agile Data Engineering Using Data Vault 2.0, Kent Graziano +- [Building a Scalable Data Warehouse with Data Vault 2.0, Dan Linstedt and Michael Olschimke](https://www.amazon.co.uk/Building-Scalable-Data-Warehouse-Vault-ebook/dp/B015KKYFGO/) +- [Better Data Modelling: An Introduction to Agile Data Engineering Using Data Vault 2.0, Kent Graziano](https://www.amazon.co.uk/Better-Data-Modeling-Introduction-Engineering-ebook/dp/B018BREV1C) ### Blogs and Downloads From 09e621cb5585675e4c635397a13597c7dd2ae093 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 1 Oct 2019 12:18:10 +0100 Subject: [PATCH 030/157] Fixed WIDV link and removed table types page - New layout to come --- README.md | 1 + docs/index.md | 2 +- docs/staging.md | 3 +++ docs/table-types/hubs.md | 0 docs/table-types/links.md | 0 docs/table-types/satellites.md | 0 mkdocs.yml | 6 ++---- 7 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 docs/staging.md delete mode 100644 docs/table-types/hubs.md delete mode 100644 docs/table-types/links.md delete mode 100644 docs/table-types/satellites.md diff --git a/README.md b/README.md index 621105dcd..f3f483b9c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ And run 1. Create a model for your hub, link or satellite 2. Set your metadata and hash model parameters 4. Call the appropriate template macro + ```bash {{- config(...) -}} diff --git a/docs/index.md b/docs/index.md index 495c71151..b1e81d0a5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -62,5 +62,5 @@ You can get further information about the Data Vault 2.0 method from the followi ### Blogs and Downloads -- [What is Data Vault?](www.data-vault.co.uk/what-is-data-vault/) +- [What is Data Vault?](https://www.data-vault.co.uk/what-is-data-vault/) - [Agile Modeling: Not an Option Anymore](https://www.vertabelo.com/blog/data-vault-series-agile-modeling-not-an-option-anymore/) diff --git a/docs/staging.md b/docs/staging.md new file mode 100644 index 000000000..7a92cd69e --- /dev/null +++ b/docs/staging.md @@ -0,0 +1,3 @@ +## Staging is the first step + +To load the vault, we must create a staging layer with all of the necessary information for loading. \ No newline at end of file diff --git a/docs/table-types/hubs.md b/docs/table-types/hubs.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/table-types/links.md b/docs/table-types/links.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/table-types/satellites.md b/docs/table-types/satellites.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/mkdocs.yml b/mkdocs.yml index d20982669..f64389202 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,10 +18,8 @@ repo_url: 'https://github.com/Datavault-UK/dbtvault' nav: - Home: 'index.md' - Getting Started: 'gettingstarted.md' - - Table types: - - Hubs: 'table-types/hubs.md' - - Links: 'table-types/links.md' - - Satellites: 'table-types/satellites.md' + - Loading the vault: + - Staging: 'staging.md' - Macros: 'macros.md' - Licence: 'LICENSE.md' From 1143d2fe559c87d6ecebff21e6ab42206590641d Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 1 Oct 2019 14:47:43 +0100 Subject: [PATCH 031/157] Added introduction to getting stated, WIP on Staging --- docs/gettingstarted.md | 12 ++++++++++++ docs/staging.md | 33 +++++++++++++++++++++++++++++++-- mkdocs.yml | 2 ++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 3af72dd21..3a61490d7 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -1,3 +1,15 @@ +## Intoduction + +This dbtvault package is very much a work in progress – we’ll up the version number to 1.0 when we’re satisfied it works out in the wild. + +We know that it deserves new features, that the code base can be tidied up and the SQL better tuned. Rest assured we’re working on it for future releases – our roadmap contains information on what’s coming. + +If you spot anything you’d like to bring to our attention, have a request for new features, have spotted an improvement we could make, or want to tell us about a typo, then please don’t hesitate to let us know. + +We’d rather know you are making active use of this package than hearing nothing from all of you out there! + +Happy Data Vaulting! :smile: + ## Prerequisites !!! note diff --git a/docs/staging.md b/docs/staging.md index 7a92cd69e..00bb73d06 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -1,3 +1,32 @@ -## Staging is the first step +We must create an appropriate staging layer with all of the necessary information for our vault. -To load the vault, we must create a staging layer with all of the necessary information for loading. \ No newline at end of file +We assume a raw staging layer already exists, all we need to do here is create hashes of these columns +for our Data Vault. + +This is where dbtvault comes in. + +### Create the staging model + +First we create a new dbt model. If our source table is called 'stg_customer' +then we should name our additional layer 'stg_customer_hashed', although any sensible naming convention will work if +kept consistent. In this case, we would create a new file 'stg_customer_hashed.sql' in our models folder. + +It is important to note that this additional layer will not necessarily be mapped to only a single table +in our Data Vault, as it may be required to map one staging table to multiple hubs, links or satellites; just keep this +in mind as we progress. + +We have our new model, what now? Let's add the model header to the file: + +```stg_customer_hashed.sql``` +```sql + +{{- config(materialized='view', schema='my_schema', enabled=true, tags='staging') -}} + +``` + +This is a simple header and you may add tags if necessary, the important parts are the materialization type and +our schema name: + +- The ```materialized``` parameter defines how our table will be materialised in our database. +Usually we want hashing layers to be views, though they can also be tables depending on our needs. +- The ```schema``` parameter is the name of the schema where this staging table will be created. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index f64389202..4cf6d461d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,8 @@ markdown_extensions: - codehilite - admonition - pymdownx.superfences + - pymdownx.emoji: + emoji_generator: !!python/name:pymdownx.emoji.to_svg - toc: permalink: true toc_depth: 1-3 From f43877e671e39782d74ce79ec73cd6b7254dbd43 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 1 Oct 2019 14:49:41 +0100 Subject: [PATCH 032/157] Removed emojigenerator from config --- mkdocs.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 4cf6d461d..b417f4c06 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,8 +40,7 @@ markdown_extensions: - codehilite - admonition - pymdownx.superfences - - pymdownx.emoji: - emoji_generator: !!python/name:pymdownx.emoji.to_svg + - pymdownx.emoji - toc: permalink: true toc_depth: 1-3 From 09210c1dd4c7cc84b3ba4f32f4da8557898d4630 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 1 Oct 2019 15:24:17 +0100 Subject: [PATCH 033/157] Added full copyright information --- LICENSE.md | 27 +-------------------------- macros/internal/create_col.sql | 14 ++++++++++++++ macros/internal/create_source.sql | 14 ++++++++++++++ macros/internal/single.sql | 14 ++++++++++++++ macros/internal/union.sql | 14 ++++++++++++++ macros/staging/add_columns.sql | 14 ++++++++++++++ macros/staging/gen_hashing.sql | 16 +++++++++++++++- macros/staging/staging_footer.sql | 14 ++++++++++++++ macros/supporting/cast.sql | 14 ++++++++++++++ macros/supporting/md5_binary.sql | 14 ++++++++++++++ macros/supporting/prefix.sql | 14 ++++++++++++++ macros/tables/hub_template.sql | 16 +++++++++++++++- macros/tables/link_template.sql | 16 +++++++++++++++- macros/tables/sat_template.sql | 16 +++++++++++++++- 14 files changed, 187 insertions(+), 30 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 427417b60..4947287f7 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -174,29 +174,4 @@ incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/macros/internal/create_col.sql b/macros/internal/create_col.sql index cd61f25d4..50fac0f48 100644 --- a/macros/internal/create_col.sql +++ b/macros/internal/create_col.sql @@ -1,3 +1,17 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro create_col(column, alias) -%} {{ column }} AS {{ alias }} diff --git a/macros/internal/create_source.sql b/macros/internal/create_source.sql index 00131ca69..b9486794a 100644 --- a/macros/internal/create_source.sql +++ b/macros/internal/create_source.sql @@ -1,3 +1,17 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro create_source(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk, source, is_union) -%} diff --git a/macros/internal/single.sql b/macros/internal/single.sql index 3a3b78058..c27619374 100644 --- a/macros/internal/single.sql +++ b/macros/internal/single.sql @@ -1,3 +1,17 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro single(src_pk, src_nk, src_ldts, src_source, tgt_pk, source, letter='a') -%} diff --git a/macros/internal/union.sql b/macros/internal/union.sql index 75feac860..bc254e5b9 100644 --- a/macros/internal/union.sql +++ b/macros/internal/union.sql @@ -1,3 +1,17 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro union(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk, source) -%} SELECT {{ tgt_cols|join(", ") }}{% if is_incremental() or union -%}, diff --git a/macros/staging/add_columns.sql b/macros/staging/add_columns.sql index 6f32e2972..201c4cbd8 100644 --- a/macros/staging/add_columns.sql +++ b/macros/staging/add_columns.sql @@ -1,3 +1,17 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro add_columns(pairs) -%} {% for pair in pairs -%} diff --git a/macros/staging/gen_hashing.sql b/macros/staging/gen_hashing.sql index f12867302..4a6a28e60 100644 --- a/macros/staging/gen_hashing.sql +++ b/macros/staging/gen_hashing.sql @@ -1,5 +1,19 @@ -{%- macro gen_hashing(pairs) -%} +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro gen_hashing(pairs) -%} +-- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault SELECT {% for pair in pairs -%} diff --git a/macros/staging/staging_footer.sql b/macros/staging/staging_footer.sql index 953594b65..fc0a8c91a 100644 --- a/macros/staging/staging_footer.sql +++ b/macros/staging/staging_footer.sql @@ -1,3 +1,17 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro staging_footer(loaddate, source, source_table) -%} {%- if source or loaddate -%}, {%- endif -%} {%- if loaddate -%} {{ loaddate }} AS LOADDATE, {%- endif -%} {%- if source -%} '{{ source }}' AS SOURCE {%- endif %} FROM {{ source_table }} diff --git a/macros/supporting/cast.sql b/macros/supporting/cast.sql index 4f368ebfa..f9e014af1 100644 --- a/macros/supporting/cast.sql +++ b/macros/supporting/cast.sql @@ -1,3 +1,17 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro cast(columns, prefix=none) -%} {#- If a string or list -#} diff --git a/macros/supporting/md5_binary.sql b/macros/supporting/md5_binary.sql index 0c8382957..f7d44db08 100644 --- a/macros/supporting/md5_binary.sql +++ b/macros/supporting/md5_binary.sql @@ -1,3 +1,17 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro md5_binary(columns, alias) -%} {%- if columns is string -%} diff --git a/macros/supporting/prefix.sql b/macros/supporting/prefix.sql index b6643011e..feb7f61a7 100644 --- a/macros/supporting/prefix.sql +++ b/macros/supporting/prefix.sql @@ -1,3 +1,17 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro prefix(columns, prefix_str) -%} {%- for column in columns -%} diff --git a/macros/tables/hub_template.sql b/macros/tables/hub_template.sql index 526f8a84f..e8c4f0a70 100644 --- a/macros/tables/hub_template.sql +++ b/macros/tables/hub_template.sql @@ -1,9 +1,23 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro hub_template(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, source) -%} {%- set is_union = true if source|length > 1 else false -%} - +-- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_nk, tgt_ldts, tgt_source], 'stg') }} FROM ( {{ dbtvault.create_source(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk|last, diff --git a/macros/tables/link_template.sql b/macros/tables/link_template.sql index 7882baab7..d3a1b04a7 100644 --- a/macros/tables/link_template.sql +++ b/macros/tables/link_template.sql @@ -1,9 +1,23 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro link_template(src_pk, src_fk, src_ldts, src_source, tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, source) -%} {%- set is_union = true if source|length > 1 else false -%} - +-- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_ldts, tgt_source], 'stg') }} FROM ( {{ dbtvault.create_source(src_pk, src_fk, src_ldts, src_source, tgt_cols, tgt_pk|last, diff --git a/macros/tables/sat_template.sql b/macros/tables/sat_template.sql index ebec12017..cd1a7ec19 100644 --- a/macros/tables/sat_template.sql +++ b/macros/tables/sat_template.sql @@ -1,10 +1,24 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} {%- macro sat_template(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source, source) -%} - +-- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault SELECT DISTINCT {{ dbtvault.cast([tgt_hashdiff, tgt_pk, tgt_payload, tgt_ldts, tgt_eff, tgt_source], 'e') }} FROM {{ source[0] }} AS e {% if is_incremental() -%} From a65e72d74e2e21206b777e323b3d173af633d0a9 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 1 Oct 2019 15:28:13 +0100 Subject: [PATCH 034/157] Updated licence link on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3f483b9c..4e284c253 100644 --- a/README.md +++ b/README.md @@ -54,4 +54,4 @@ And run Please open an issue first to discuss what you would like to change. ## License -[Apache 2.0](https://choosealicense.com/licenses/apache-2.0/) \ No newline at end of file +[Apache 2.0](LICENSE.md) \ No newline at end of file From 47fd909fba672707ded3fc5e776475d744cfeccc Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 1 Oct 2019 16:00:22 +0100 Subject: [PATCH 035/157] Added to staging page - Also added empty Hubs/Links/Satellite pages - Updated copyright notice in footer --- docs/gettingstarted.md | 5 +++-- docs/hubs.md | 0 docs/links.md | 0 docs/satellites.md | 0 docs/staging.md | 32 ++++++++++++++++++++++++++++++-- mkdocs.yml | 8 ++++++-- 6 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 docs/hubs.md create mode 100644 docs/links.md create mode 100644 docs/satellites.md diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 3a61490d7..c1787eabc 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -4,7 +4,8 @@ This dbtvault package is very much a work in progress – we’ll up the version We know that it deserves new features, that the code base can be tidied up and the SQL better tuned. Rest assured we’re working on it for future releases – our roadmap contains information on what’s coming. -If you spot anything you’d like to bring to our attention, have a request for new features, have spotted an improvement we could make, or want to tell us about a typo, then please don’t hesitate to let us know. +If you spot anything you’d like to bring to our attention, have a request for new features, have spotted an improvement we could make, or want to tell us about a typo, +then please don’t hesitate to let us know via [Github](https://github.com/Datavault-UK/dbtvault/issues). We’d rather know you are making active use of this package than hearing nothing from all of you out there! @@ -28,4 +29,4 @@ packages: And run ```dbt deps``` -[Read more on package installation (from dbt)](https://docs.getdbt.com/docs/package-management) \ No newline at end of file +###### [Read more on package installation (from dbt)](https://docs.getdbt.com/docs/package-management) \ No newline at end of file diff --git a/docs/hubs.md b/docs/hubs.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/links.md b/docs/links.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/satellites.md b/docs/satellites.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/staging.md b/docs/staging.md index 00bb73d06..b8836dbc5 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -7,6 +7,8 @@ This is where dbtvault comes in. ### Create the staging model +#### The model header + First we create a new dbt model. If our source table is called 'stg_customer' then we should name our additional layer 'stg_customer_hashed', although any sensible naming convention will work if kept consistent. In this case, we would create a new file 'stg_customer_hashed.sql' in our models folder. @@ -15,7 +17,7 @@ It is important to note that this additional layer will not necessarily be mappe in our Data Vault, as it may be required to map one staging table to multiple hubs, links or satellites; just keep this in mind as we progress. -We have our new model, what now? Let's add the model header to the file: +We have our new model file, what now? Let's add the model header to the file: ```stg_customer_hashed.sql``` ```sql @@ -29,4 +31,30 @@ our schema name: - The ```materialized``` parameter defines how our table will be materialised in our database. Usually we want hashing layers to be views, though they can also be tables depending on our needs. -- The ```schema``` parameter is the name of the schema where this staging table will be created. \ No newline at end of file +- The ```schema``` parameter is the name of the schema where this staging table will be created. + +#### Providing the metadata + +Now we get into the core component of staging: providing metadata. +This metadata is straightforward and consists of the column names we want to hash, and the alias for our new +column containing the hash representation. + +We need to call the [gen_hashing](/macros/#gen_hashing) macro and provide the appropriate parameters. This macro takes +our provided column names and generates all of the necessary SQL for us. More on how to use this macro is +provided in the link above. + +After adding the macro call, our model will now look something like this: + +```stg_customer_hashed.sql``` +```sql + +{{- config(materialized='view', schema='my_schema', enabled=true, tags='staging') -}} + +{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, + +``` + +!!! note + Make sure you add the trailing comma after the call. + +#### Additional columns \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index b417f4c06..c52c16267 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,6 +20,9 @@ nav: - Getting Started: 'gettingstarted.md' - Loading the vault: - Staging: 'staging.md' + - Hubs: 'hubs.md' + - Links: 'links.md' + - Satellites: 'satellites.md' - Macros: 'macros.md' - Licence: 'LICENSE.md' @@ -37,7 +40,8 @@ extra: link: 'https://www.facebook.com/DataVaultUK/' markdown_extensions: - - codehilite + - codehilite: + linenums: true - admonition - pymdownx.superfences - pymdownx.emoji @@ -48,4 +52,4 @@ markdown_extensions: extra_css: - 'stylesheets/cube.css' -copyright: dbtvault and docs © Datavault 2019 | dbt is a registered trademark of Fishtown Analytics \ No newline at end of file +copyright: dbtvault and documentation © Business Thinking trading as Datavault 2019 - Data Vault 2.0 is a registered trademark of Dan Linstedt - dbt is a registered trademark of Fishtown Analytics From 6739206c899516ce4146b9300daff5fe3de4188a Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 1 Oct 2019 16:09:21 +0100 Subject: [PATCH 036/157] Removed licence appendix --- docs/LICENSE.md | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/docs/LICENSE.md b/docs/LICENSE.md index 427417b60..4947287f7 100644 --- a/docs/LICENSE.md +++ b/docs/LICENSE.md @@ -174,29 +174,4 @@ incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + END OF TERMS AND CONDITIONS \ No newline at end of file From 17825b11b64cc8262ed55d322930beca543ae8c6 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 1 Oct 2019 17:04:40 +0100 Subject: [PATCH 037/157] Finished the Staging page --- docs/staging.md | 101 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/docs/staging.md b/docs/staging.md index b8836dbc5..394471b5e 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -33,13 +33,13 @@ our schema name: Usually we want hashing layers to be views, though they can also be tables depending on our needs. - The ```schema``` parameter is the name of the schema where this staging table will be created. -#### Providing the metadata +#### Providing the metadata for hashing Now we get into the core component of staging: providing metadata. This metadata is straightforward and consists of the column names we want to hash, and the alias for our new column containing the hash representation. -We need to call the [gen_hashing](/macros/#gen_hashing) macro and provide the appropriate parameters. This macro takes +We need to call the [gen_hashing](macros.md#gen_hashing) macro and provide the appropriate parameters. This macro takes our provided column names and generates all of the necessary SQL for us. More on how to use this macro is provided in the link above. @@ -48,13 +48,102 @@ After adding the macro call, our model will now look something like this: ```stg_customer_hashed.sql``` ```sql -{{- config(materialized='view', schema='my_schema', enabled=true, tags='staging') -}} - -{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} + +{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, ``` !!! note Make sure you add the trailing comma after the call. -#### Additional columns \ No newline at end of file +#### Additional columns + +Our Data Vault will not just consist of hashes, so we will need to add some additional columns to our new staging layer, +containing concrete data. + +With the [add_columns](macros.md#add_columns) macro, we can provide a list of columns and any corresponding aliases for +those columns. + + +```stg_customer_hashed.sql``` +```sql + +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} + +{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, + +{{ dbtvault.add_columns([('CUSTOMER_ID', 'CUSTOMER_ID'), + ('CUSTOMER_DOB', 'CUSTOMER_DOB'), + ('CUSTOMER_NAME', 'CUSTOMER_NAME'), + ('LOADDATE', 'LOADDATE'), + ('LOADDATE', 'EFFECTIVE_FROM')]) }} + +``` + +!!! note + In future releases, this step shouldn't be necessary, as dbtvault will automatically include + the rest of the columns found in our staging table for us. + +#### Adding the footer + +Finally, we need to provide a fully qualified source table name for our new staging layer to get data from. +In this example, this would be ```MYDATABASE.MYSCHEMA.stg_customer``` where ```MYDATABASE.MYSCHEMA``` is the +database and schema in your Snowflake database where your raw staging table resides. + +This can be achieved without any SQL, by using the [staging_footer](macros.md#staging_footer) macro. + +Explained in the documentation, this macro also has ```loaddate``` and ```source``` parameters. These are to simplify +the creation of ```SOURCE``` and ```LOADDATE``` columns. The parameters can be omitted in favour of adding them via the +[add_columns](macros.md#add_columns) macro, as showcased in the snippet above with ```LOADDATE```. + +After adding the footer, our completed model should now look like this: + + +```stg_customer_hashed.sql``` +```sql + +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} + +{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, + +{{ dbtvault.add_columns([('CUSTOMER_ID', 'CUSTOMER_ID'), + ('CUSTOMER_DOB', 'CUSTOMER_DOB'), + ('CUSTOMER_NAME', 'CUSTOMER_NAME'), + ('LOADDATE', 'LOADDATE'), + ('LOADDATE', 'EFFECTIVE_FROM')]) }} + +{{- dbtvault.staging_footer(source="STG_CUSTOMER", + source_table='MYDATABASE.MYSCHEMA.stg_customer_hashed') }} + +``` + +!!! tip + In the call to [staging_footer](macros.md#staging_footer) we have provided ```STG_CUSTOMER``` as the value for the + ```source``` parameter, this will give every record in our new staging layer the value + ```STG_CUSTOMER``` as its ```SOURCE```. + This will allow us to trace this data back to the source once it is loaded into our vault from our new staging layer. + + It is entirely optional, and if you already have a source column you can simply add it using + [add_columns](macros.md#add_columns) instead. + +#### Running dbt + +With our model complete, we can run dbt and have our new staging layer materialised as configured in the header: + +```dbt run --models stg_customer_hashed``` + +And our table will look like this: + +| CUSTOMER_PK | CUSTOMER_ID | CUSTOMER_DOB | CUSTOMER_NAME | LOADDATE | EFFECTIVE_FROM | SOURCE | +| -------------------------------- | ------------ | ------------- | -------------- | ---------- | -------------- | ------------ | +| B8C37E33DEFDE51CF91E1E03E51657DA | 1001 | 1997-04-24 | Alice | 1993-01-01 | 1993-01-01 | STG_CUSTOMER | +| . | . | . | . | . | . | . | +| . | . | . | . | . | . | . | +| FED33392D3A48AA149A87A38B875BA4A | 1004 | 2018-04-13 | Dom | 1993-01-01 | 1993-01-01 | STG_CUSTOMER | + + +#### Next... + +Now that we have implemented a new staging layer with all of the required fields and hashes, we can start loading our vault +with hubs, links and satellites. \ No newline at end of file From dea7df1d1bbfa3b884c5cd33ee4382baaa32930e Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 1 Oct 2019 20:35:26 +0100 Subject: [PATCH 038/157] Fixed headers on Staging page --- docs/staging.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/staging.md b/docs/staging.md index 394471b5e..368c782e6 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -1,3 +1,4 @@ + We must create an appropriate staging layer with all of the necessary information for our vault. We assume a raw staging layer already exists, all we need to do here is create hashes of these columns @@ -5,9 +6,8 @@ for our Data Vault. This is where dbtvault comes in. -### Create the staging model -#### The model header +### The model header First we create a new dbt model. If our source table is called 'stg_customer' then we should name our additional layer 'stg_customer_hashed', although any sensible naming convention will work if @@ -33,7 +33,7 @@ our schema name: Usually we want hashing layers to be views, though they can also be tables depending on our needs. - The ```schema``` parameter is the name of the schema where this staging table will be created. -#### Providing the metadata for hashing +### Providing the metadata for hashing Now we get into the core component of staging: providing metadata. This metadata is straightforward and consists of the column names we want to hash, and the alias for our new @@ -57,7 +57,7 @@ After adding the macro call, our model will now look something like this: !!! note Make sure you add the trailing comma after the call. -#### Additional columns +### Additional columns Our Data Vault will not just consist of hashes, so we will need to add some additional columns to our new staging layer, containing concrete data. @@ -85,7 +85,7 @@ those columns. In future releases, this step shouldn't be necessary, as dbtvault will automatically include the rest of the columns found in our staging table for us. -#### Adding the footer +### Adding the footer Finally, we need to provide a fully qualified source table name for our new staging layer to get data from. In this example, this would be ```MYDATABASE.MYSCHEMA.stg_customer``` where ```MYDATABASE.MYSCHEMA``` is the @@ -127,7 +127,7 @@ After adding the footer, our completed model should now look like this: It is entirely optional, and if you already have a source column you can simply add it using [add_columns](macros.md#add_columns) instead. -#### Running dbt +### Running dbt With our model complete, we can run dbt and have our new staging layer materialised as configured in the header: @@ -143,7 +143,9 @@ And our table will look like this: | FED33392D3A48AA149A87A38B875BA4A | 1004 | 2018-04-13 | Dom | 1993-01-01 | 1993-01-01 | STG_CUSTOMER | -#### Next... +### Next steps Now that we have implemented a new staging layer with all of the required fields and hashes, we can start loading our vault -with hubs, links and satellites. \ No newline at end of file +with hubs, links and satellites. + +Click next below! \ No newline at end of file From 146a151a6e39cfb3098551e881ecb065218a8de4 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 1 Oct 2019 20:50:44 +0100 Subject: [PATCH 039/157] Minor corrections --- docs/staging.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/staging.md b/docs/staging.md index 368c782e6..704903932 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -22,7 +22,7 @@ We have our new model file, what now? Let's add the model header to the file: ```stg_customer_hashed.sql``` ```sql -{{- config(materialized='view', schema='my_schema', enabled=true, tags='staging') -}} +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} ``` @@ -50,12 +50,12 @@ After adding the macro call, our model will now look something like this: {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} -{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, +{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, ``` !!! note - Make sure you add the trailing comma after the call. + Make sure you add the trailing comma after the call on line 3. ### Additional columns @@ -103,18 +103,18 @@ After adding the footer, our completed model should now look like this: ```stg_customer_hashed.sql``` ```sql -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} - -{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, - -{{ dbtvault.add_columns([('CUSTOMER_ID', 'CUSTOMER_ID'), - ('CUSTOMER_DOB', 'CUSTOMER_DOB'), - ('CUSTOMER_NAME', 'CUSTOMER_NAME'), - ('LOADDATE', 'LOADDATE'), - ('LOADDATE', 'EFFECTIVE_FROM')]) }} - -{{- dbtvault.staging_footer(source="STG_CUSTOMER", - source_table='MYDATABASE.MYSCHEMA.stg_customer_hashed') }} +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} + +{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, + +{{ dbtvault.add_columns([('CUSTOMER_ID', 'CUSTOMER_ID'), + ('CUSTOMER_DOB', 'CUSTOMER_DOB'), + ('CUSTOMER_NAME', 'CUSTOMER_NAME'), + ('LOADDATE', 'LOADDATE'), + ('LOADDATE', 'EFFECTIVE_FROM')]) }} + +{{- dbtvault.staging_footer(source="STG_CUSTOMER", + source_table='MYDATABASE.MYSCHEMA.stg_customer') }} ``` @@ -124,7 +124,7 @@ After adding the footer, our completed model should now look like this: ```STG_CUSTOMER``` as its ```SOURCE```. This will allow us to trace this data back to the source once it is loaded into our vault from our new staging layer. - It is entirely optional, and if you already have a source column you can simply add it using + It is entirely optional, and if you already have a source column in your raw vault you can simply add it using [add_columns](macros.md#add_columns) instead. ### Running dbt From 5209071110060e04a4125411f07e380702ee6962 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 2 Oct 2019 00:10:38 +0100 Subject: [PATCH 040/157] Completed Hubs page - Also updated staging page headers to fit/match hub page headers. --- docs/hubs.md | 185 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/staging.md | 10 +-- 2 files changed, 190 insertions(+), 5 deletions(-) diff --git a/docs/hubs.md b/docs/hubs.md index e69de29bb..317891405 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -0,0 +1,185 @@ +Hubs are one of the core building blocks of a Data Vault. + +In general, they consist of 4 columns: + +1. A primary key (or surrogate key) which is usually a hashed representation of the natural key (also known as the business key). + +2. The natural key itself. This is usually a formal identification for the record such as a customer ID or order number. + +3. The load date or load date timestamp. This identifies when the record was first loaded into the vault. + +4. The source for the record. (i.e. ```STG_CUSTOMER``` from the [previous section](staging.md#adding-the-footer)) + +### Creating model header + +Create a new dbt model as before. We'll call this one 'hub_customer'. + +The following header will be appropriate, but feel free to customise it to your needs: + +```hub_customer.sql``` +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} + +``` + +An incremental materialisation will optimize our load in cases where the target table (in this case, ```hub_customer```) +already exists and already contains data. This is very important for tables containing a lot of data, where every ounce +of optimisation counts. + +[Read more about incremental models](https://docs.getdbt.com/docs/configuring-incremental-models) + +!!! note "Dont worry!" + The [hub_template](macros.md#hub_template) will deal with the filtering of records and ensuring all of the Data Vault + 2.0 standards are upheld when loading into the hub from the source. We won't need to worry about unwanted duplicates. + +### Adding the metadata + +Let's look at the metadata we need to provide to the [hub_template](macros.md#hub_template) macro. + +#### Source columns + +Using our knowledge of what columns we need in our ```hub_customer``` table, we can identify which columns in our +staging layer we will need: + +1. We need a primary key, which is a hashed natural key. The ```CUSTOMER_PK``` we created is a perfect fit. +2. We also need the natural key itself, ```CUSTOMER_ID``` which we added using the [add_columns](macros.md#add_columns) macro. +3. A load date timestamp is needed, which we also added to the staging layer as ```LOADDATE``` +4. We also added a ```SOURCE``` column. + +We can now add this metadata to the model: + +```hub_customer.sql``` +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_nk = 'CUSTOMER_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +``` + +#### Target columns + +Now we can define the target column mapping. The [hub_template](macros.md#hub_template) does a lot of work for us if we +provide the metadata it requires. We can define which source columns map to the required target columns and also +define a column type at the same time: + +```hub_customer.sql``` +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_nk = 'CUSTOMER_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = [src_pk, src_nk, src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} +{%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +``` + +With these 5 additional lines, we have now informed the macro how to transform our source data: + +- On line 8, we have written the 4 columns we worked out earlier to define exactly what source columns +we are using and what order we want them in. We have used the variable references to avoid writing the columns again. +- On the remaining lines we have provided our mapping from source to target. We don't want to change the names of the +columns, so we have used the source column reference on both sides. +- We have provided a type in the mapping so that the type is explicitly defined. + +!!! info + There is nothing to stop you entering incorrect type mappings in this step, so please ensure they are correct. + You will soon find out, however, as dbt will issue a warning to you. No harm done, but save time by providing + accurate metadata! + + +!!! question "Why is ```tgt_cols``` needed?" + In future releases, we hope to eliminate the need to duplicate the source columns as shown on line 8. + + For now, this is a necessary evil. + +#### Source table + +The last piece of metadata we need is the source table. This step is easy, as in this example we created the +new staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. +dbt ensures dependencies are honoured when defining the source using a reference in this way. + +[Read more about the ref function](https://docs.getdbt.com/docs/ref) + +```hub_customer.sql``` + +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_nk = 'CUSTOMER_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = [src_pk, src_nk, src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} +{%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_customer_hashed')] -%} +``` + +### Invoking the template + +Now we bring it all together and call the [hub_template](macros.md#hub_template) macro: + +```hub_customer.sql``` + +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_nk = 'CUSTOMER_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = [src_pk, src_nk, src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} +{%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_customer_hashed')] -%} + +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} +``` + +### Running dbt + +With our model complete, we can run dbt to create our ```hub_customer``` hub. + +```dbt run --models +hub_customer``` + +!!! tip + The '+' in the command above will cause dbt to also compile and run all parent dependencies for the model we are + running, in this case, it will re-create the staging layer from the ```stg_customer_hashed``` model if needed. + +And our table will look like this: + +| CUSTOMER_PK | CUSTOMER_ID | LOADDATE | SOURCE | +| -------------------------------- | ------------ | ---------- | ------------ | +| B8C37E33DEFDE51CF91E1E03E51657DA | 1001 | 1993-01-01 | STG_CUSTOMER | +| . | . | . | . | +| . | . | . | . | +| FED33392D3A48AA149A87A38B875BA4A | 1004 | 1993-01-01 | STG_CUSTOMER | + + +### Next steps + +We have now created a staging layer and a hub. Next we will look at Links, which are created in a similar way. + +Click next below! \ No newline at end of file diff --git a/docs/staging.md b/docs/staging.md index 704903932..d5f101ce0 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -7,11 +7,11 @@ for our Data Vault. This is where dbtvault comes in. -### The model header +### Creating the model header -First we create a new dbt model. If our source table is called 'stg_customer' -then we should name our additional layer 'stg_customer_hashed', although any sensible naming convention will work if -kept consistent. In this case, we would create a new file 'stg_customer_hashed.sql' in our models folder. +First we create a new dbt model. If our source table is called ```stg_customer``` +then we should name our additional layer ```stg_customer_hashed```, although any sensible naming convention will work if +kept consistent. In this case, we would create a new file ```stg_customer_hashed.sql``` in our models folder. It is important to note that this additional layer will not necessarily be mapped to only a single table in our Data Vault, as it may be required to map one staging table to multiple hubs, links or satellites; just keep this @@ -33,7 +33,7 @@ our schema name: Usually we want hashing layers to be views, though they can also be tables depending on our needs. - The ```schema``` parameter is the name of the schema where this staging table will be created. -### Providing the metadata for hashing +### Adding the metadata Now we get into the core component of staging: providing metadata. This metadata is straightforward and consists of the column names we want to hash, and the alias for our new From 92b75f829c9e84b2ebe768ead88a1bb8963259a5 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 2 Oct 2019 00:26:31 +0100 Subject: [PATCH 041/157] Added PDF exporter to requirements --- .gitignore | 1 + docs/requirements.txt | 3 ++- mkdocs.yml | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..45ddf0ae3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +site/ diff --git a/docs/requirements.txt b/docs/requirements.txt index 2f2e07978..877459416 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ mkdocs-material==4.4.2 mkdocs-minify-plugin==0.2.1 pygments -pymdown-extensions \ No newline at end of file +pymdown-extensions +mkdocs-pdf-export-plugin \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index c52c16267..d92a40cba 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,6 @@ site_name: dbtvault site_author: Datavault +site_dir: 'site' theme: name: 'material' custom_dir: 'theme' @@ -52,4 +53,8 @@ markdown_extensions: extra_css: - 'stylesheets/cube.css' +plugins: + - search + - pdf-export + copyright: dbtvault and documentation © Business Thinking trading as Datavault 2019 - Data Vault 2.0 is a registered trademark of Dan Linstedt - dbt is a registered trademark of Fishtown Analytics From d382c45295f7d2d667911a1b89d64e73105bfd23 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 2 Oct 2019 00:29:58 +0100 Subject: [PATCH 042/157] Removed 'click next' references --- docs/hubs.md | 4 +--- docs/staging.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/hubs.md b/docs/hubs.md index 317891405..3de45c94e 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -180,6 +180,4 @@ And our table will look like this: ### Next steps -We have now created a staging layer and a hub. Next we will look at Links, which are created in a similar way. - -Click next below! \ No newline at end of file +We have now created a staging layer and a hub. Next we will look at Links, which are created in a similar way. \ No newline at end of file diff --git a/docs/staging.md b/docs/staging.md index d5f101ce0..f7ca7f1fb 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -146,6 +146,4 @@ And our table will look like this: ### Next steps Now that we have implemented a new staging layer with all of the required fields and hashes, we can start loading our vault -with hubs, links and satellites. - -Click next below! \ No newline at end of file +with hubs, links and satellites. \ No newline at end of file From 8cff2d82e718d8ff55d4e152e73ced6af6420d6f Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 2 Oct 2019 00:34:28 +0100 Subject: [PATCH 043/157] Removed PDF exporter for now --- docs/requirements.txt | 3 +-- mkdocs.yml | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 877459416..2f2e07978 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,4 @@ mkdocs-material==4.4.2 mkdocs-minify-plugin==0.2.1 pygments -pymdown-extensions -mkdocs-pdf-export-plugin \ No newline at end of file +pymdown-extensions \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index d92a40cba..794ebdde1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,8 +53,4 @@ markdown_extensions: extra_css: - 'stylesheets/cube.css' -plugins: - - search - - pdf-export - copyright: dbtvault and documentation © Business Thinking trading as Datavault 2019 - Data Vault 2.0 is a registered trademark of Dan Linstedt - dbt is a registered trademark of Fishtown Analytics From 4a6a7935e9fc898332d2bac09d1b22d14ad5fc27 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 2 Oct 2019 12:17:57 +0100 Subject: [PATCH 044/157] Finished Links page and updated pre-requisites - Minor corrections to hubs page as well --- docs/gettingstarted.md | 9 ++ docs/hubs.md | 18 ++-- docs/index.md | 2 +- docs/links.md | 208 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 227 insertions(+), 10 deletions(-) diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index c1787eabc..17ef42101 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -16,6 +16,15 @@ Happy Data Vaulting! :smile: !!! note These requirements are subject to change as we improve the package. +1. Some prior knowledge of Data Vault 2.0 architecture. Have a look at +[How can I get up to speed on Data Vault 2.0?](index.md#how-can-i-get-up-to-speed-on-data-vault-20) + +2. A Snowflake account, trial or otherwise. [Sign up for a free 30-day trial here](https://trial.snowflake.com/ab/) + +2. We assume you already have a raw staging layer. + +3. Our macros assume that you are only loading from one set of load dates in a single load cycle. We will be removing this + restriction in future versions. ## Installation diff --git a/docs/hubs.md b/docs/hubs.md index 3de45c94e..96fb4c6cd 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -10,7 +10,7 @@ In general, they consist of 4 columns: 4. The source for the record. (i.e. ```STG_CUSTOMER``` from the [previous section](staging.md#adding-the-footer)) -### Creating model header +### Creating the model header Create a new dbt model as before. We'll call this one 'hub_customer'. @@ -41,15 +41,15 @@ Let's look at the metadata we need to provide to the [hub_template](macros.md#hu Using our knowledge of what columns we need in our ```hub_customer``` table, we can identify which columns in our staging layer we will need: -1. We need a primary key, which is a hashed natural key. The ```CUSTOMER_PK``` we created is a perfect fit. -2. We also need the natural key itself, ```CUSTOMER_ID``` which we added using the [add_columns](macros.md#add_columns) macro. -3. A load date timestamp is needed, which we also added to the staging layer as ```LOADDATE``` -4. We also added a ```SOURCE``` column. +1. A primary key, which is a hashed natural key. The ```CUSTOMER_PK``` we created is a perfect fit. +2. The natural key itself, ```CUSTOMER_ID``` which we added using the [add_columns](macros.md#add_columns) macro. +3. A load date timestamp, which we also added to the staging layer as ```LOADDATE``` +4. A ```SOURCE``` column. We can now add this metadata to the model: ```hub_customer.sql``` -```sql +```sql hl_lines="3 4 5 6" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} {%- set src_pk = 'CUSTOMER_PK' -%} @@ -66,7 +66,7 @@ provide the metadata it requires. We can define which source columns map to the define a column type at the same time: ```hub_customer.sql``` -```sql +```sql hl_lines="8 10 11 12 13" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} {%- set src_pk = 'CUSTOMER_PK' -%} @@ -112,7 +112,7 @@ dbt ensures dependencies are honoured when defining the source using a reference ```hub_customer.sql``` -```sql +```sql hl_lines="15" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} {%- set src_pk = 'CUSTOMER_PK' -%} @@ -136,7 +136,7 @@ Now we bring it all together and call the [hub_template](macros.md#hub_template) ```hub_customer.sql``` -```sql +```sql hl_lines="17 18 19" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} {%- set src_pk = 'CUSTOMER_PK' -%} diff --git a/docs/index.md b/docs/index.md index b1e81d0a5..3286977d0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,7 +52,7 @@ Later versions will extend the range of table types that the package handles. We If you are going to use the dbtvault package for your Data Vault 2.0 project, then we expect you to have some prior knowledge about the Data Vault 2.0 method. -## How Can You Get Up to Speed on Data Vault 2.0? +## How Can I Get Up to Speed on Data Vault 2.0? You can get further information about the Data Vault 2.0 method from the following resources: ### Books (from Amazon) diff --git a/docs/links.md b/docs/links.md index e69de29bb..d5baeeeb5 100644 --- a/docs/links.md +++ b/docs/links.md @@ -0,0 +1,208 @@ +Links are another fundamental components in a Data Vault. + +Links model an association or link, between two business keys. + +They are similar to [hubs](hubs.md) in structure but contain one additional column, which is simply an additional business key. + +!!! note + Due to the similarities between links and hubs, most of this page will be familiar if you have already read the + [hubs](hubs.md) page. + +Our links will contain: + +1. A primary key. This is a concatenation of the two foreign keys below, hashed. +2. A foreign key holding the business key for one source table. +3. Another foreign key holding they business key from an associated source table. +4. The load date or load date timestamp. +5. The source for the record + +### Creating the model header + +Create another dbt model. We'll call this one 'link_customer_nation'. + +The following header will be appropriate, but feel free to customise it to your needs: + +```link_customer_nation.sql``` +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} + +``` + +### Getting required columns + +In the [staging](staging.md) walk-through, we added all the columns we needed for creating our hub. +To create our link, we will now need to add some additional columns to our staging layer. + +An individual source table should contain all of the necessary columns to create links, as raw staging +tables often contain multiple associations between keys (that's why we're creating links!). + +Because of this, we can simply add some additional pairs to the [add_columns](macros.md#add_columns) macro call. + +In this scenario we will be creating a link to model the association between a customer and their nation, so we +add the following lines if ```stg_customer``` contains a ```NATION_ID``` column: + +```stg_customer_hashed.sql``` +```sql hl_lines="4 5 7" + +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} + +{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK'), + (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), + ('NATION_ID', 'NATION_PK')]) -}}, + +{{ dbtvault.add_columns([('CUSTOMER_ID', 'CUSTOMER_ID'), + ('NATION_ID', 'NATION_ID'), + ('CUSTOMER_DOB', 'CUSTOMER_DOB'), + ('CUSTOMER_NAME', 'CUSTOMER_NAME'), + ('LOADDATE', 'LOADDATE'), + ('LOADDATE', 'EFFECTIVE_FROM')]) }} + +{{- dbtvault.staging_footer(source="STG_CUSTOMER", + source_table='MYDATABASE.MYSCHEMA.stg_customer') }} + +``` + +!!! note + We can rename the staging layer to ```stg_customer_nation_hashed.sql``` if we want to keep + consistent naming standards, just make sure the reference is updated wherever it is used. + +### Adding the metadata + +Now we need to provide some metadata to the [link_template](macros.md#link_template) macro. + +#### Source columns + +Using our knowledge of what columns we need in our ```hub_customer``` table, we can identify which columns in our +staging layer we will need: + +1. A primary key, which is a combination of the two foreign keys: ```CUSTOMER_NATION_PK``` from our modified staging layer. +2. ```CUSTOMER_ID``` which is one of our foreign keys +3. ```NATION_ID``` the second foreign key. +3. A load date timestamp, which is in the staging layer as ```LOADDATE``` +4. A ```SOURCE``` column. + +We can now add this metadata to the model: + +```link_customer_nation.sql``` +```sql hl_lines="3 4 5 6" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} + +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +``` + +!!! note + We are using ```src_fk```, a list of the foreign keys. This is instead of ```src_nk``` when building the hubs. + +#### Target columns + +Now we can define the target column mapping. The [link_template](macros.md#link_template) does a lot of work for us if we +provide the metadata it requires. We can define which source columns map to the required target columns and also +define a column type at the same time: + +```link_customer_nation.sql``` +```sql hl_lines="8 10 11 12 14 15" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} + +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +``` + +!!! note + The column name strings on lines 8 and 11 could easily be replaced with references to + the ```src_pk``` and ```src_fk``` variables, these are just written in full for clarity. + + +#### Source table + +The last piece of metadata we need is the source table. As we did with the hubs, we can reference +the staging layer model we made earlier: + +```link_customer_nation.sql``` + +```sql hl_lines="17" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} + +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_customer_nation_hashed')] -%} +``` + +### Invoking the template + +Now we bring it all together and call the [link_template](macros.md#link_template) macro: + +```link_customer_nation.sql``` +```sql hl_lines="19 20 21" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} + +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', src_ldts, src_source] -%} + +{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_customer_nation_hashed')] -%} + +{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} + +``` + +### Running dbt + +With our model complete, we can run dbt to create our ```link_customer_nation``` link. + +```dbt run --models +link_customer_nation``` + +And our table will look like this: + +| CUSTOMER_NATION_PK | CUSTOMER_ID | NATION_ID | LOADDATE | SOURCE | +| -------------------------------- | ------------ | ------------ | ---------- | ------------ | +| 72A160C6CDBF0EDC9D4B4398796C9B42 | 1001 | 10001 | 1993-01-01 | STG_CUSTOMER | +| . | . | . | . | . | +| . | . | . | . | . | +| 1CE6A9D2688B0DB0893E46BEDECBF1E3 | 1004 | 10004 | 1993-01-01 | STG_CUSTOMER | + + +### Next steps + +We have now created a staging layer, a hub and a link. Next we will look at satellites. +These are a little more complicated, but don't worry, the [sat_template](macros.md#sat_template) will handle that for +us! From bd1512e8ef19322640f7786899aa369e34eb44e1 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 2 Oct 2019 16:32:29 +0100 Subject: [PATCH 045/157] Fixed "See also" --- docs/macros.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/macros.md b/docs/macros.md index 6f3f7468d..c8e44de01 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -390,7 +390,7 @@ ___ [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) -!!! seealso +!!! seealso "See Also" [md5_binary](#md5_binary) A macro for generating multiple lines of hashing SQL for columns: From 45cf8824a2cf221b62e6d86a7c1c0d2a99e0fb08 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 2 Oct 2019 22:47:55 +0100 Subject: [PATCH 046/157] Updated usage --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4e284c253..3b4e900f2 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ And run ## Usage 1. Create a model for your hub, link or satellite -2. Set your metadata and hash model parameters -4. Call the appropriate template macro +2. Provide metadata +3. Call the appropriate template macro ```bash {{- config(...) -}} @@ -54,4 +54,4 @@ And run Please open an issue first to discuss what you would like to change. ## License -[Apache 2.0](LICENSE.md) \ No newline at end of file +[Apache 2.0](LICENSE.md) From c61f995b609f5d1f4e837963111f5f88f72a575e Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 09:26:21 +0100 Subject: [PATCH 047/157] Re-named hashing macros - gen_hashing -> multi_hash - md5_binary -> hash --- docs/links.md | 2 +- docs/macros.md | 14 +++++++------- docs/staging.md | 8 ++++---- macros/staging/{gen_hashing.sql => multi_hash.sql} | 4 ++-- macros/supporting/{md5_binary.sql => hash.sql} | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) rename macros/staging/{gen_hashing.sql => multi_hash.sql} (91%) rename macros/supporting/{md5_binary.sql => hash.sql} (96%) diff --git a/docs/links.md b/docs/links.md index d5baeeeb5..00e581455 100644 --- a/docs/links.md +++ b/docs/links.md @@ -46,7 +46,7 @@ add the following lines if ```stg_customer``` contains a ```NATION_ID``` column: {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} -{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK'), +{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), ('NATION_ID', 'NATION_PK')]) -}}, diff --git a/docs/macros.md b/docs/macros.md index c8e44de01..3da5d484d 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -381,7 +381,7 @@ ___ These macros are intended for use in the staging layer ___ -### gen_hashing +### multi_hash !!! warning This macro ***should not be*** used for cryptographic purposes. @@ -391,7 +391,7 @@ ___ [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) !!! seealso "See Also" - [md5_binary](#md5_binary) + [hash](#hash) A macro for generating multiple lines of hashing SQL for columns: ```sql @@ -411,7 +411,7 @@ CAST(MD5_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(16)) AS alias2 #### Usage ```yaml -{{ dbtvault.gen_hashing([('CUSTOMERKEY', 'CUSTOMER_PK'), +{{ dbtvault.multi_hash([('CUSTOMERKEY', 'CUSTOMER_PK'), (['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF')]) }} ``` @@ -556,7 +556,7 @@ CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE ___ -### md5_binary +### hash !!! warning This macro ***should not be*** used for cryptographic purposes @@ -585,12 +585,12 @@ CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias #### Usage ```yaml -{{ dbtvault.md5_binary('CUSTOMERKEY', 'CUSTOMER_PK') }}, -{{ dbtvault.md5_binary(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF') }} +{{ dbtvault.hash('CUSTOMERKEY', 'CUSTOMER_PK') }}, +{{ dbtvault.hash(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF') }} ``` !!! tip - [gen_hashing](#gen_hashing) may be used to simplify the hashing process and generate multiple hashes with one macro. + [multi_hash](#multi_hash) may be used to simplify the hashing process and generate multiple hashes with one macro. #### Output diff --git a/docs/staging.md b/docs/staging.md index f7ca7f1fb..5ed829f5d 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -39,7 +39,7 @@ Now we get into the core component of staging: providing metadata. This metadata is straightforward and consists of the column names we want to hash, and the alias for our new column containing the hash representation. -We need to call the [gen_hashing](macros.md#gen_hashing) macro and provide the appropriate parameters. This macro takes +We need to call the [multi_hash](macros.md#multi_hash) macro and provide the appropriate parameters. This macro takes our provided column names and generates all of the necessary SQL for us. More on how to use this macro is provided in the link above. @@ -50,7 +50,7 @@ After adding the macro call, our model will now look something like this: {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} -{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, +{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, ``` @@ -71,7 +71,7 @@ those columns. {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} -{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, +{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, {{ dbtvault.add_columns([('CUSTOMER_ID', 'CUSTOMER_ID'), ('CUSTOMER_DOB', 'CUSTOMER_DOB'), @@ -105,7 +105,7 @@ After adding the footer, our completed model should now look like this: {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} -{{ dbtvault.gen_hashing([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, +{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK')]) -}}, {{ dbtvault.add_columns([('CUSTOMER_ID', 'CUSTOMER_ID'), ('CUSTOMER_DOB', 'CUSTOMER_DOB'), diff --git a/macros/staging/gen_hashing.sql b/macros/staging/multi_hash.sql similarity index 91% rename from macros/staging/gen_hashing.sql rename to macros/staging/multi_hash.sql index 4a6a28e60..892d60301 100644 --- a/macros/staging/gen_hashing.sql +++ b/macros/staging/multi_hash.sql @@ -12,12 +12,12 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro gen_hashing(pairs) -%} +{%- macro multi_hash(pairs) -%} -- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault SELECT {% for pair in pairs -%} - {{ dbtvault.md5_binary(pair[0], pair[1]) }} + {{ dbtvault.hash(pair[0], pair[1]) }} {%- if not loop.last -%} , {% endif %} {% endfor %} diff --git a/macros/supporting/md5_binary.sql b/macros/supporting/hash.sql similarity index 96% rename from macros/supporting/md5_binary.sql rename to macros/supporting/hash.sql index f7d44db08..a75cd5968 100644 --- a/macros/supporting/md5_binary.sql +++ b/macros/supporting/hash.sql @@ -12,7 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro md5_binary(columns, alias) -%} +{%- macro hash(columns, alias) -%} {%- if columns is string -%} From 2f5f0a3490c628814e62e3648d4c05771869f9ca Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 09:45:09 +0100 Subject: [PATCH 048/157] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..57dab8e1f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at enquiries@data-vault.co.uk. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq From d980a2ed06360c992494d82eb6e89aba3f88e51b Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 09:59:40 +0100 Subject: [PATCH 049/157] Added contributing guidelines --- CONTRIBUTING.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..916b0a716 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +## We'd love to hear from you + +This dbtvault package is very much a work in progress – we’ll up the version number to 1.0 when we’re satisfied it +works out in the wild. + +We know that it deserves new features, that the code base can be tidied up and the SQL better tuned. +Rest assured we’re working on it for future releases – our roadmap contains information on what’s coming. + +If you spot anything you’d like to bring to our attention, have a request for new features, +have spotted an improvement we could make, or want to tell us about a typo, then please don’t hesitate to let us know +by submitting an issue using the below guidelines + +We’d rather know you are making active use of this package than hearing nothing from all of you out there! + +Happy Data Vaulting! + +## Issue guidelines + +### If it's a bug +We've tested the package rigorously, but if you think you've found a bug please provide the following so we can fix it +as quickly as possible: + +- The version of dbtvault being used (as we're still in pre-release, this can be omitted) +- Steps to reproduce the issue +- Any error messages or dbt log files which can give more detail of the problem + +### If it's a feature request +We'd love to add new features to make this package even more useful for the community, +please feel free to submit ideas and thoughts! + +## Pull requests +If you've developed something which we can add via a pull request, we'd prefer that you submit an issue first +so that we can discuss the changes. \ No newline at end of file From efcff8bdd533a00c5b7418273233a82cf0fde886 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 10:03:15 +0100 Subject: [PATCH 050/157] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 30 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..f5c243eb7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] " +labels: '' +assignees: DVAlexHiggs + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Log files** +If applicable, provide dbt log files which include the problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..3448c4715 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE] " +labels: '' +assignees: DVAlexHiggs + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From f559f8a0c04dcc98bbaf23e535731f3ff1a3bc9c Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 10:04:38 +0100 Subject: [PATCH 051/157] Updated contribution guidelines --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 916b0a716..ff30417b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,8 +17,8 @@ Happy Data Vaulting! ## Issue guidelines ### If it's a bug -We've tested the package rigorously, but if you think you've found a bug please provide the following so we can fix it -as quickly as possible: +We've tested the package rigorously, but if you think you've found a bug please provide the following +at a minimum (or use the issue templates) so we can fix it as quickly as possible: - The version of dbtvault being used (as we're still in pre-release, this can be omitted) - Steps to reproduce the issue From 33d1a70c12e5af1b23085d0cb9dce4edaa8e8986 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 10:12:33 +0100 Subject: [PATCH 052/157] Added sign-up to README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b4e900f2..ef7016cba 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses; powered by [dbt](https://www.getdbt.com/), a registered trademark of [Fishtown Analytics](https://www.fishtownanalytics.com/) +## Sign up for early-bird announcements + +[SIGN UP](https://www.data-vault.co.uk/dbtvault/) and get notified of new features and new releases +before anyone else! ## Currently supported databases: @@ -51,7 +55,7 @@ And run ``` ## Contributing -Please open an issue first to discuss what you would like to change. +[View our contribution guidelines](CONTRIBUTING.md) ## License [Apache 2.0](LICENSE.md) From 0ad07ee88db06d24f9f984608e40de30bfaef2d4 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 10:13:22 +0100 Subject: [PATCH 053/157] Moved sign-up down --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ef7016cba..915164509 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,6 @@ dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses; powered by [dbt](https://www.getdbt.com/), a registered trademark of [Fishtown Analytics](https://www.fishtownanalytics.com/) -## Sign up for early-bird announcements - -[SIGN UP](https://www.data-vault.co.uk/dbtvault/) and get notified of new features and new releases -before anyone else! - ## Currently supported databases: - [snowflake](https://www.snowflake.com/about/) @@ -54,6 +49,11 @@ And run source) }} ``` +## Sign up for early-bird announcements + +[SIGN UP](https://www.data-vault.co.uk/dbtvault/) and get notified of new features and new releases +before anyone else! + ## Contributing [View our contribution guidelines](CONTRIBUTING.md) From ecabebf1299d0f6651c33654e147203c88aabfff Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 10:19:15 +0100 Subject: [PATCH 054/157] Added sign-up message to docs --- docs/index.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 3286977d0..2c82f8293 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,6 +10,13 @@ dbtvault is a dbt package that implements a Data Vault 2.0 Data Warehouse on a S dbt is designed for ease of use in data engineering: for when you need to develop a data pipeline. It is a single executable that can run on your desktop or a VM in your network, it is developed in Python, and is free to download and use. +!!! tip + #### Sign up for early-bird announcements + + [Sign up](https://www.data-vault.co.uk/dbtvault/) and get notified of new features and new releases + before anyone else! + + ## What is Data Vault 2.0? Data Vault 2.0 is an Agile method used to deliver a highly scalable enterprise Data Warehouse. @@ -63,4 +70,4 @@ You can get further information about the Data Vault 2.0 method from the followi ### Blogs and Downloads - [What is Data Vault?](https://www.data-vault.co.uk/what-is-data-vault/) -- [Agile Modeling: Not an Option Anymore](https://www.vertabelo.com/blog/data-vault-series-agile-modeling-not-an-option-anymore/) +- [Agile Modeling: Not an Option Anymore](https://www.vertabelo.com/blog/data-vault-series-agile-modeling-not-an-option-anymore/) \ No newline at end of file From 0864274e93ad17e2d1665b46e4fd79b6cbce3379 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 11:28:54 +0100 Subject: [PATCH 055/157] Added placeholder demo page to docs --- docs/demonstration.md | 6 ++++++ mkdocs.yml | 1 + 2 files changed, 7 insertions(+) create mode 100644 docs/demonstration.md diff --git a/docs/demonstration.md b/docs/demonstration.md new file mode 100644 index 000000000..2ae998e18 --- /dev/null +++ b/docs/demonstration.md @@ -0,0 +1,6 @@ +## Coming soon + +We will soon be making available a downloadable example project running dbtvault with the Snowflake TPCH dataset. +This will showcase dbtvault with pre-written models, giving you further understanding of how it all works. + +[Sign up](https://www.data-vault.co.uk/dbtvault/) and get notified when this is available! \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 794ebdde1..a4ab6f2cb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,6 +25,7 @@ nav: - Links: 'links.md' - Satellites: 'satellites.md' - Macros: 'macros.md' + - Demonstration: 'demonstration.md' - Licence: 'LICENSE.md' extra: From f2127683b70b602cff06a5f2a328568a6f42ed7b Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 11:59:51 +0100 Subject: [PATCH 056/157] Added Roadmap and Changelog --- docs/changelog.md | 27 +++++++++++++++++++++++++++ docs/gettingstarted.md | 2 +- docs/roadmap.md | 39 +++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 ++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 docs/changelog.md create mode 100644 docs/roadmap.md diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 000000000..62f32b5de --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,27 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1pre] - 2019-09 / 2019-10 +### Added + +- Table Macros: + - [Hub](macros.md#hub_template) + - [Link](macros.md#link_template) + - [Satellites](macros.md#sat_template) + +- Supporting Macros: + - [cast](macros.md#cast) + - [hash](macros.md#hash) (renamed from md5_binary) + - [prefix](macros.md#prefix) + +- Staging Macros: + - [add_columns](macros.md#add_columns) + - [multi_hash](macros.md#multi_hash) (renamed from gen_hashing) + - [staging_footer](macros.md#staging_footer) + +### Documentatiom + +- Numerous changes leading up to Version 1.0 release \ No newline at end of file diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 17ef42101..3ff6c2736 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -2,7 +2,7 @@ This dbtvault package is very much a work in progress – we’ll up the version number to 1.0 when we’re satisfied it works out in the wild. -We know that it deserves new features, that the code base can be tidied up and the SQL better tuned. Rest assured we’re working on it for future releases – our roadmap contains information on what’s coming. +We know that it deserves new features, that the code base can be tidied up and the SQL better tuned. Rest assured we’re working on it for future releases – [our roadmap contains information on what’s coming](roadmap.md). If you spot anything you’d like to bring to our attention, have a request for new features, have spotted an improvement we could make, or want to tell us about a typo, then please don’t hesitate to let us know via [Github](https://github.com/Datavault-UK/dbtvault/issues). diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 000000000..29dec07a2 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,39 @@ +With each release we will be adding more Data Vault 2.0 tables and helpful macros. +We hope to tailor new features to the requirements of our community, making the package +the best and most useful it can be. + +#### Contribute to dbtvault + +- Do you have some ideas? [Let us know what you want added](https://github.com/Datavault-UK/dbtvault/issues) +- Want to contribute your own work? [Read our contribution guidelines](https://github.com/Datavault-UK/dbtvault/blob/master/CONTRIBUTING.md) + +## Release 1.0 + +We're currently working towards release 1! + +Everything is ready to go and can be used as it is, we're just cleaning up some of the rough edges and making sure the +documentation is up to scratch. + +Release 1 will include: + +#### Tables + +- Staging +- Hubs +- Links +- Satellites + +#### Supporting Macros + +- cast +- hash +- prefix + +## Release 2.0 + +#### Tables + +- Transactional Links +- PITs +- Bridges +- And more diff --git a/mkdocs.yml b/mkdocs.yml index a4ab6f2cb..3cb743c58 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,8 @@ nav: - Satellites: 'satellites.md' - Macros: 'macros.md' - Demonstration: 'demonstration.md' + - Roadmap: 'roadmap.md' + - Changelog: 'changelog.md' - Licence: 'LICENSE.md' extra: From b31efb0dc401054b2d1cae23d66c456026823edf Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 3 Oct 2019 17:02:27 +0100 Subject: [PATCH 057/157] Major improvements to documentation - Have re-written and clarified many sections - Removed the need to edit the staging models in the link section, added this to the staging section instead for a better flow. - Improved wording in many areas - Added to improvements on roadmap - Added a helpful diagram to the staging page - Small typo corrections --- docs/assets/images/staging.png | Bin 0 -> 14322 bytes docs/changelog.md | 4 +- docs/gettingstarted.md | 11 ++- docs/hubs.md | 57 ++++++------ docs/index.md | 43 ++++++--- docs/links.md | 124 ++++++++++++-------------- docs/roadmap.md | 16 +++- docs/staging.md | 153 +++++++++++++++++++++------------ 8 files changed, 231 insertions(+), 177 deletions(-) create mode 100755 docs/assets/images/staging.png diff --git a/docs/assets/images/staging.png b/docs/assets/images/staging.png new file mode 100755 index 0000000000000000000000000000000000000000..007a1ac670e5a0c8adf63f528ad640fef017f3c1 GIT binary patch literal 14322 zcmd6Ogu-= z@*Usz_xm5-eLmZso##2RbDwiR*L~gB6ZT$1k&Kv*7ytl}DJ#9z1^{p&*yBsW``FKu z7pc#%UqmiSh9CfdgyQavGqD7@0sxo+%5UX9c;g^59USw4zB8yfU~&>)q<;PDN0tm9 zbd_|ao*qU~M;(U>Tnqk)v7J-+ttD4YC{@F0{+@*h5d+v{HqtWcegy2!)qX)i)jlR( zS|ZNR(qc#U zfA&?Y%ogJEP+pvz#bgWgy9F0J&=e-_uHqyKd>IqEGyC7 z23<29DRP9k(8=3=d+6H=SJau_lo&xbv7QBqv4mKiM%ih9m{01{Cd_Tw61WGbeKS{~ zh8gPJtcdG+)*%~N!O}F1UOT2leD(+j_W55vDDu_-^hv}|qaTZK`XJ)1Dq<;XUlh2?Z4u;!(gqR~71V_q>aat4he!)8h7z z*Ztdmh2|C4Y@{6CXP4s|x6NTp6VT@4H~QA{vw2e#h7sK$Ff zDq)qty&KSpW5G&a*_oy^##OrTur1gjl--#&|%3*ec>H`HvA=aaLERe7zB2slAK}IVXw!=G_bwtFU zgWn0J3kIK^v4=QHEyT9r{~Y`H!_6l!6eak z*9WI9XUI1Tuts$EnD{~mE8bm0!<~2yeS*bQMJZ;0+QIubj`|0@%#HYm;ttyy@&^kp z5OB3OT7ejn2^#9{@phPgFIn)@z?K(C{weQa);2;+xcxy2UsAcuZu~aSb<3;O-N3mq z-R3O84FzKB$g0$ib>El-Vfe{ON7ykD}qFj(5EbW@bE z=9fuKy=_OKUN68zM;Ko$^HIl-@Ze)Qhwvjth(;z>aM^pFtO)NPLQe>VonwvmQm;gq zRy=JsmSN|M?Fc{FUrRg-YVxbtb&dJOzN5wCmJqiF|6`fWr~o$GZgaE^s+JBu8(qEy z;Y!5T>9Wa(N9A2)Trrkn&B0xw9vy>@%Tbs~+cU%7=!` z`H{kn_W%IuZ};r{Bq#RY!^NcPGkYpTx~21`se?l_soS35b&>-B?+Vx0PamuKrm_-$ zBr~18pc1HFeb!a%W7fz)h;@X5+)4ZSp+y3dvzJ_IhL`V*Qb@n7U`L#PWU>&S|KifX zH_@(WHVti=qvz~6wG$-;e&lfaP6%>x>$b>oBelId_~L(?p*~d}f$mxT!n76|*dY=f zTlJh167u?Rqw`)jw{02OPc&h-bcfo{DSCYU=NH}qRdb0&{1C}YzhPBbZM)s(3zI^! z(kc4KcA~4$-uW4SQvK6cMfBm9Cs#E~KF0gf_{nMPW||!PlvO+^KKv*qY->ZL#i_72= zveQPC;6OyWPkux^j5H%_g6@!(?L2StQ z7p;)~^8ainD3>>yG7j@NJGp4bot*-SS3z}t=5YS`oV6Pxsd zXwxZ(r6?|Z=i$Ygq$9IW4>9@~MTa&O%ihR*$T2CMEVhl5C)hbl67l_ZG#)_*6*V0V z$Nb$kC#klzlM+6yv_p7D7IJc`DEZ}rjVhB|de6Mh_ioSL$pjSd*X=(G2a(-1lJX>c z+GHxk^aF8}c&dPEdR;DJ=rfN!@Iiic4jev(%2sh}%Ou6L@x-t2QT-lInEuHekJyg^ ziZuYW<_n>DDhvG{cy?LA&y-=mhA+j0T@=}SGJVGQmuT>Z_Rzyt`>pQhtQ~o~5ZUP# z6&Q-0C>?lfRts-yL2Kxp{%HMFQ|1}#_YjK(ERv`)SFDRf(=+%s)2b$_x$7)G22?dY zKQ>eDtq!|LY{?D>4Z#r3M1+5hVx&MDzo3KH(@$!FsyIZNkFT__YhEDEipo8bC+JPn z-u6o4poo*sTREZLjT?Hdf3R4 z%lYT|AyrVp%sv;6tBo1A!Wvm>mT@>K|Bx{;6CQSU`*3^1o>X!0r3eh_x{aq5F-m(D z8BYFAWNn>0bD*eKN&8JOMGLhOYh`AyI~rt79Hciz=%$gUdg{mT(!cH8>5;^lbdW{T z5<-M6m8AMb@7zEP{_^n*GgH%`Wa*2hRA^qj3{zh@qn6tO2seG z+0+-zS4OZ~s<_zPE~oT`N~iVrrIedY=()`OjyJFGreP=+J(jw9`x6fa>pyMxoNr6M z7(VI(Dv{{BEXdHM=J5^G#oKad?t}dGb)X!>g-eJaN_U(d;$W<6u#QzfhJk**8WQ7Q zH&}qz`uk898kULqYNSu^>KqLU#_!~WlaI!ssQ)&xXA8Ep`>{PpLRj=qd}oz`T<^Q) zzR%k@YfVrdq*yz=Gl(U`$&R__oo~fa_l_iA>dcPe2cW9~u6goVevbVkeb)BScS0rv zxZcw!3@`G=j6>es=lemElH;=C?vv~pa3~N>c(CD zI3NLCwtfEeqZpN$1IG@DRf&P4(S<~2nB;3)Vc@^mt4HJww?+PV>RbMWfVkyAI;cxJ zW$GM(of`|zoqw!EdR*vG*8Iybdlw9pIE=F$vPurODRFr?+|l4`)s2}=?cp*T zyrlRO`E{u6F$XP_6|d;=Q%|;sM(v$C_}?5=-`LK~=vFqKDjXkz!pd9hemUBi%&?hw zB`~*?iAcmw(Rv``AkId3usE<4zo+z*)f(P6K2ls?a3gv{_q{WayUSNw=9o>yNvAX9 z;2ui|dB80UGD+@_Lfq^N3~!Mc>{r^@yYq_}r+tT>-nt6zAA0^-Rj&ZHi123G>D9VO zEPl<>m*ed2C8T+Q(%{x#EI~S(te&?;uTmJM2exsuj83d){LE~N)e~VynZ+@!6x~pV zZu8*};ql>RsdAb>WBMm*du8E--kuIXqerj*jNUeo%iMM%nj0I@)~!|Mw}l9F`?V$$ zBml{w7my%E79;D=Rbl6bU~mGf1V;@$b0{!*=3w5lKWpc~XYiTLBv~UuS9E~l!Tuyt0XRw(a0v}#6!x;WRjuvFmR|HEDUNixcZQqIm4$i+iqkEhiJseQ& z7yH8I=u^Z+BGc9JEjs#W0|)RaLbbcj4nyK_`n4+JO6?|^FZKGeaL|W6cT{_AukVi^ z&nfxJDuKjCe{a^+;lp3O*NoEF8^wR6+AkM=!TdM18ygA#CTrgMp_{TVFbBr)!-W}o z(-6e*-{kfqMR`K_z$F17MS^GIPg}*5Rkiy;(<5?S=xAwU!A*?R(Ieg+ajh6>oqzjH zSp7A={jH5oWx9d=4Y=C8?H;@j=%byu z0IQ4-zXG4Ln_i|X7?@uJvEbspJ>zO9z7OZYryM(PExCg)O@AphvqH{qSN;C(wqxwj zYZE9ErpvVGP;%Mxg}-IyXiM}OhiqrWeOjjW)+ay1YllImOCpo0(wvx+>a7i}XW1zF z*tQbvB!IfR^%0>0Hz?TOW7*5Hzp9(rBl=+6jy~yZFP={gKTdWa-U~eJ+DY;<2cPG{&&;o# z;na|gVhGFC{H@RJ4(t{a0jIv{Cckw=7v8?LAm~W3<0LTdPjva>_UiLA!2R# zo^*JY{b`o?->Uq#ErRq}DhIXxo{U5m@s_HPEVkm)2G5`X^LN-eUpxZKo_xMplD$y9 ztv~5Lhoi~)mrebP;nB;RGPLf-w?~xOSHI5#&Q^rsedz?=B8<8^wkm@ker*a+o>(tu zx`XF+IjrX;yv9y!=<#IxL<0_)Aexc1XLxYfBfi!2Mm&{uZ=ccpY z(BlZ2@m3izxhqB8*pU~Mi-Lb`Zf{4-3ng(VYxObJL$8Y#|4B%Y0aXsjjqn7bI6R zGvDD&054&RFH8KNov7Lf3Z5wK96ys;(LI!?vO4x02!~F#kDG&^W46EAp?r!yo}0$w zA1F@!KpJ~e2kZ6S)GL;4OCMt1rcV1{i`a>Z%=+*qigXi6_VkXl&d-&2(#)|aU6;tJ z<>I!`?hb6bUM41fF5Qe9e0Pkjxp^w;+lGarFof5&=c@FjpSoKj4&XJv+$w>vqWqE% z;YU-pz>8GRRp+Yu;ifa6%8M9<>)Wq}je5=o;|F;^;6Iz=2qsqW_3gPy(N8Zju<-(O zGBa9^dN$_We)o+T$gnV58+r<(nfC-?`;JLM3Em(?TqnlxiNbp+=I8g(4*4m-$>T9g zY{ja(ir!qgJ1=~p=6F@9pBje$#>gM=5&6q)%+1VuNfG-s_&&D>kbCa}0MYD5aszLA ztooP!sjA#AptOCFm&Gus=2(|UMgJi9&$ivzJ-#H+A zPiQY*ee8U)cGby!yFUf7B;@0bT*QrkswlqIfA1#xur00yH_!-CH{gYeusCs0uD{`G z**evXva<}>LIdMl?Jz%tpGES&sG^8EIQkA;lUs#xDs7c39iuH((iBr3*@&j(>QY;< zG`f80eFnYC#2nZ5IM7zq|LEkSyIu_!pC|5?V9GSCRe}jMk)B+#0y2?a3t^uF>++p%@c>1I*nX z6gh(s>v&3hWQqBBht_LDQ#VFT1K+<-_@wuYt-_rph+(c5cW|t|ldS-R14tpGwsX>2 zmtjg9kDqgtvPu(ec{kx43Y&G|>RRiY`6 z3W@f6A~BxLjX`g zi7w5q(WX5f%cT>-3_Y8|g&;F~hx7Ba;}|L#nL3?@@y&~wOHP3`Ocp(ggagS~FNsfU?~gUF^>>x}_6vS(ey|NMZbF`(t1T0QXm($6W|tTuqz^3kW) z`tr+h)Xa<=fKR&j$N&=0Xk;K0TU3k8wh#lP3$n$Ay(EkM{Kt*nxJBv(Xz75UW!Ec( z0UN0{VBG-^--4>$VbVqYm!goi62`fyTl0Z)ltGdCB>wL@c zoa@R3I|UL7{`qas9Yx)?FIiXPMqm6HOc6UftuUzS#YQ!<72~_&NzzjzqidI~Mwyss z0s+LlIm$b9ukYzOE76L3aXm_-01Q({%9p z^LTjFy0imVbDW|JDJNvrRTn&NoOZ5F6ZA1cP&KWJCj^V^q(21uWs-IyJFqbr5Gv%akR z7<=KSo5C8dXPxP^BG_?|+I``p%D^S!{a&Ew$(|VN_ihWCs_ROl?8$R=)4`(3?hu*X z^abA@1yfD`Z>9=nH12!~sc5eTB#$Yj32fYk9GH zucCiDk-bLs*qnjZx?Pk-&~rrPkF9O+IlL=jOv-*P$4(x0zl)yToQS#mx3u}TPNR(d zz38*^G}l{>zw?R*#FzYXB`K@}e9E+@1J(VooY#+y3*O3c+s}r({+d8RtDDkH>R;RM zKINepQ9B+do*9Mkw9~W%aHO2qkL`pyuwWK(El;GyPI!~!m1{Y2ZA|0cx|{US34DiU z%Uzu}_t~#9w^M<GB<0Xnb7xg0AAlI#CU`weXqNy@D5? zQ{m%-GTTfKPGPl@8>Nyf&|@B{X*g`;UcBP3&P6^(At1{S<`~6?mSWFIXgV|Vzvslc99c0+gmVl!L<4H zzntEDFd1(v7r5dTpB3^z)x5qAU<8jdKarXVw4JKKA1;ELDpQBPBJzDq6vE!v=0cJT z?0$%~XL^L^P47)wsZV|TBl`irK= z%_Pi)(*HDT@My>OgysD3{}^vhWyD_q_RT6^CdfMnm$X7?Bzu~h$gR=Ji$1_g>rBkN zE}!?aId^qRt4@5%x8`$oGLZME(7!3nrip=ZkYGv*pfP*<<)jAg(#BKg?eRvSRei(o zOzo9HM>f~JJ&b{rFpE!@zji)=CKL);y?HfaJcaAXH&<2o5_r?EpG1hr{&!fMNXppJ z=ePc&=vb`zs5h>vHVe%`xhDS#{82sE))#OJK3A`WYw1eoXoOxRhgky%f0272DnhFa@QEvCz=hp z3;eUTI&_`Tdhj-g!Pkr?M~n77`Qp60a(mh1OQkYf5{+lGA4}Iv4}@8qB(}w`^*Vb; z3|}>B0B-`*_oZI%kN^Jg!4CG}67!-BUV;<*b#mgnBbP5s#FFagX@y%U5z$O+-}I=) zl(~5!u3EOOWg{k1ENuV!JFCU2L2wc#W`6eRhwvI?@M{$fT8C4Is_2eIR^Ovz;}-~! zer!?siS9PTeLVp)c*Ln+2|9aw_ui3lC5rXkQKdwdOjqV1KFUN&>Su^R-#OS$K%HJ#|gW_<@ zh$XAeQM);|frc?OQHWeMc`?c2D@k4zSP61S>#R*C)5mzBaHPJ4x?6m1AyDFX>3~`t z(%tmcOaeWa&HfqHN3_ZUuO{MX$sHPD&%vVD7@Ns3io|u@Kb6#v>5=Yv!YC(^zJ79oXiPD$2q@@C(s9jvbmo zUT*3%K09GpFYRlQ|7CkxfvNzh?f6w8&XhdfNhjlt`A-yoK2qnZH_;m`kMI$N%H)&) zo46DuoEu-xy`0Qf|K2-bz$zV_#^$Mk?SSw2bzZtfO`}UDB#W!X z?=<^)^3|!OmbO#JYv`W`Jx@es{_D}E7C4yTR~1N;YB9JvFvkVj&$L-t^z8n)5EZMn zfN1PNDWnom)8WYqQvd8S&YOmX9e90RA;9e<|3nnbw8)I=WCYX|6|RKP@DB(yst92t ztyFCA#ophfnkyWg;yjwVpO|5frS~y>0eC4KnFgB#LzJFIikM1W6 zB{to)3%)5gE1a6#Qvvv|;&r+bhuEB>bo9TS$w0m{a+F+zl!d= zl@5qJMoQ;nL%~8`!RABZH-W)N`B_#L%1pGWX3q%kZ#sXLUjY^C z`Y8=iIdjXXL+EELQkLI*O3}`qRZ+F-G_i6bx4}U9tyG2>lAO33k32|}E0!Bm{>&D< zfaGTLu2_fK=pSFT_qH+UU{SM`fRC7da5nKTXYLZfQ3R3oIg*o|xu|F*^c`F7OWp5J zKa%zgr)kqAiZoT^U8TkztcNcw-rG%DBo|Zao3Ee*8KHMEl)KxL6-M=xbOTU6_-@Yo{*n2ed@n$3Q zkEP{l3U)xSji*x(ezLUWuS>o4`$A&D*-wiw!5%iBkYCdU`k`c`Umq1QcCb zK8jZLtOQ1*5{23IKwHz!0o5MaslD~KKYw1K;I`$mIrS5LcH zBUJKbynp45jPp`2_797$I8##IxUrz^+$5DVZ?JTt=w7KR0fkBKVdI3|?)pp0zk)Ke zrq0Kj=B1Fj1lFWltGUel3X~`K(B#`qx5I7X+rat^=NsR7$>{Yq`W92Bjc@K2;Yf2J zP+dM=0P373>rk~JTf2Xm(BMJusOVTWO#rjeYrZfm6U&+p+&D8=rnVWjPS3iE`{5zb zl2Y~}!tN#?OYPVWg{}71^puKcwSGS?X=myyEaM^jYXOAk^USB5Ij}UoGTeB8 zVepkLog2yTgG*znSMRg8e*CPcw3mwTtZbHZ;#s7+m>(emo6a<9sP>VW=FR_oR=^WaTe$4G>5BCl1o?ImJggr?8-S{}FXL35R3m^^NSMUjtF7uwL7e(RaK1%jI%j>)yA6EVSIojTK1dV)z{h^?;~E8+v}-e8 zHGW-~79r+1dr|P1*Dkbr1G?K6)P(%gWvB8e-hqVjTI0e|=_o`|uxqY9-yZHyhu$mlLz?*&lD zMK`5fQfKZOd5tsU^K<)YoZQD*u$u1Qc!H?xRHeZe&Y1oRkKT4Ux{z6M$&t~LJ$5)f z1plX1Ei1}J^H^b&BsmT(wQu9~jdijfm7=`DU#`w*tLxpQea1d7)o3y+UXiu_US!gB zp5y7H-oo;*T)S8&8N~BPC6nNkS2OtNC2oi1_&B=S(yKxNXl#|j^f>{yndInvFUmMj^OK4?QaL8-f#_;D(_T;u_=zWt`yTW718+U zuIdW-o%2V1sp}GpPae}?bxzMPySOeSB0Y~}XhXfU`5Y{4)&##tYPG;;k!D{kN0Phb zhj-tjUPZdRS=O35m=6LrV& z%Nk`y?+m2~D66`HYP_7*gXH0l#cn@x6n+LJCq6uU7fxcQIzPdi)qU;mc#NXg$5lK2 z8NRdiJabct3?>{reV;mklYz@{i)7Pp26RvH@COHI?S9?Yj3`+);Khf9QoJ@GS=Odx zZ&IpHaYFcgz^B37<0*2wNod#UqN7}*K*MYB-^CA}i5YV}`Buo?-eRQn;e4zfYMPea zW&$b_Jj%x4q-yN77t)=4(e}3zZ1`e86$DZ~!*1i|WGunpzd9o2=@vhtK>2pCz+a&) z!;V5FQ~1>eLwkm;yp!4we%%PsB6WRTOTWmUtSb8qY!zxlj<5D)08Tk)d->hbf3ixy zJhLz*rlogpOB*}(=x%SziJuGP zmhLv3?l}QBi1o_HA|_yLe;0iQpEg6HigjuPgQ16 z5LG9J#fRhU1DM|WMFizSF}um~j?%oVxfxr!Lshz!FHlrlQ_$g7!K`i z&+ZQx1EsO|f$>Z+!kKJZ(yxAc8MBs#N^zSCUVqkU*V)jVJ-$4{>~A+Z5LNy$=8{lU*p%?XVFO%Wyj8sc z^ATx43GMp}b7?BoDadA+%E9x&C2OWE))xmhGw<2xeg*s@OlJLiCH@T zF#r(x%~kIkC&v`8Td1o2LJWIXdZ+(-bYZWpM1c(#7b3eV=KWZRFZ1!d_HtJ4s{F0# z$*;g$VSAmti&a^yOsG3UfLWMui zxA=Fd+DECM(b?>;BIcWTEZvxinYqqEM#-P*9y=AM;-pCGIF^-3ZWuM^#MRZ=?QT;7 z09JrbR9eWK_HK%IpVaO&s`SPY zD^w6iP0B2`ZBo<$RnzBigNPx&NDQBU$lK|B1Xp?2hP=v`bPDF{AN{FnM3W+l?X=|u z{pIV#oK@qT{*e^iGJr*UiGAd+3~5|K$S<3vO3s1tdWPPC+9f*0wB!`cz8%tC=_E5N z{TuDKg7WMs?~HY+uW9xf+SL{v@)kh~aH9f2@I;_|AU!i$LZnko8m5`%O~YJN3LPZ}uc8#7`Bjp-N#hqZq;ob>ERwHc8^YcXaeRB>dScys~hmGsc(_IJ;eod+>>yT zH|GcGjtEt-;`n`1Z?A;x>mnDo%*(Z76J1QDX1*^x&cvLxyJy5qCmPNsSMoNldRH{0 zg7e|Wg=1LG({e#zU381|e-&L@+&$%wlwy?qpAJn)n>PAyzX(+uRHcxvu--7Ft~%wX zmOs#`%;v)oFMhtPg>uL~9Ae9dx&qVb6Zb=;(11KhSi3zqz#Axs`77RddIWB((`1-=b9i%C5e|XmkGq^7`WRe22@d@h>(M zz4A}J-)7S`L@huG10E*$a<~ml_DKVYc-KiuwzVAm3z`>S=@->t#aNyNrhaVd###%- z4BRVuDJj5S1uaN?`oO7i0!G~Hg}jppWMW#YoN>h{g7I3-Nw*b{f}2!E4m3aW$>$j= zSJt*+NoH-$EMT8qOzdLl>QPLuT#Grmtkogr;~>ua z8mv0>r=lWOwvXvKJG@eY4oT{Yv zO0=dQJD(rn#m0P3o|DT!kiRn;Dm3-eemqxqaY#e!ndl)^+{)ty0qw{SQ2Mm1i)Xp0 z-|E$#WUn$mue1XwEf(;t4g4b81=vr3rQ|8mgJ~CiLG_?4kA~eOsY+n_SvF67U~M~W z^+IPao&!9BO=CV`KBkjR)yeVrB_1oF}s5)ktXRa}XyEr#f2L=eA`fzT@F_ z=_%-}XwTCYyf&X+;zlOU60oi)u(Duh5k9qU_hnS?+EI%i6DL*8ltZR!TT35e3#6(8a?V5a@$TC~xhU)Kgi zuFiyF_Nrx0EC?-jd{i-zI1qRZo(w6ZJxM@^CfAGa(1&3IB8|nSJfbssHrG4^ zn99#^v(i>p=h+FCg1xO{ZTS*yttq32yF5poKGh(?9qjOkFJSMdmnhp6F zZ2ae)fGW@XxR-xFi0blG-k$906NJZz$#w*@=Y4GA(-)DzfFm?WYJZV@lSz^F927DC z`#g6?2`O>|Rsc4adNw$wz2<1`_6$giY_Z91=u{}K+Vy{|D!X8LyMaGCykq!6jHjY$l$G?j}2kh5#j!9_imp~h@4 zGZ0HflFn}ypqJ%EKXi{#28?C4Va}ANKKk%WZ9#|qMjIHN@iCVfpeShGq3e{uVHe8G z?W0eqPTj6jVU6Kf?QSp#ETWqC95 zN_^3SByMUA4HO{cmVDKSADvmX2l~(h&SUl>H;0QU-{=eP&U-Anu^oZFT!ftFe!PKt zGLFmZit^|z%04AGm-Xgt8b|+}JfWL~`1)gf-r7EO@g) z1oQ@<+YplH0;}{>#ve!^z<_xk#~ao?DA0u)mDH33_r{$|V{g*{FN)CN>$VLNqLf~W zS(RGq%+tg1mZ18O&C1rl&@yUKy#g)~WLuL~iZSKQ+5zX$(S{zhJ#`HzEf_O$wNLOErV31Op_D;bk z=xV*(7U`r;8L)f#HDUl<5pZc^YsK##?*6buH*3Ad*T9Es0~&Z`I84R7Zv343uVP47f`5cz8^ zH6a-`c4@x69!Yp=H-~#k71Ua`YXG*kJTcy!5EhRKG>J950Mon;1`tN$-l?J0@M*}F zb$t`nlc6@d^VP}KmUEjRWNJx4@@61cOJ%7F2G-Ol?kMNYpHee zSkMsXmAzo( zQ2IUJfdBO4#`m=LB7S?Xai;qw!bF|XI2JWEYBx&?0S`y&8Yk=+nNSX!$BvWL-<7SPr|bDP7x+-HY2M6|ApvdKi~ zsQpBX;BD&qhlZ_KS!RbLXJcC7>YwjX zEqYZAQ^y2*#_kP%80*bR|3Lgo@gAWFqqZkD8FcWRDYJ}&napDW-UkXCC&xFkMnJDq zle~Bjo_jNRlik#>Gb0^3)#>ew^0g|bL;|L7Ni+tDCi6Lf4*IL8HzJiANk;}qkx#ey zd0PekS83GIUxXZ3HiGzyl{;@eYB-fZZY-Sv8Ii`TG#S%Qi!;QS&Ypwb3%vP8uiq`^ zq-kpIB+B@?Z~@4Xvy4SRpTNeM%gZ3Cl#mtJAt6X$jj5{9C$b6>)?{(WDQ*rI4uOZ} zgt!P(2~g#$8NfC@-6-|f%_k6551mPrQ<9ETYg!isN>TEtUW7$QNcuEz5 z2nIE|vFwl)nvAf0zST4NL)r*+xywt>=!wsAhrbZUdD5Fdo-u;0uZ|@&)5Mk8ze16Q)LIzhJM8T_~CPY^I?hEA%6F3 z7T*_}cFwIfAR8`XfvccCtzbpp9e9$lSPoJ@jish{e(E%#*P)|+|DDY7p`631pf->z z8-s35d?-$a1BnYZcOgn`^TQ>H$Kd1&<%S zAKQhhL>|@|jW7XEzf3BwY4{O-QV-iSOB<6QHp=lv+NhqZAVxXZqQoGb?NJD9Zs9eS zxE(H_8B6?a^KI1M|A&DiKl|kLUBu%pKKZ-tMWKoW8&S|`&GcGQWvQP<4OT?f_ay1r zRv^uP&rKoFDkRp*cRMKs#2ZZ~s&ywa+h8!K@tA13cXw>jlTwA9N0I-^vxP(c+il%B zF>CYr6L(Krbuj2C4Y(Kr-3PeubZSFfi7XA{T*G&JRPft}gvB|MpaSq+<^L Date: Thu, 3 Oct 2019 20:39:36 +0100 Subject: [PATCH 058/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 915164509..737fa0847 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ # dbtvault by [Datavault](https://www.data-vault.co.uk) -dbtvault is a DBT package for creating Data Vault 2.0 compliant Data Warehouses; +dbtvault is a dbt package that generates & executes the ETL you need to run a Data Vault 2.0 Data Warehouse on a Snowflake database; powered by [dbt](https://www.getdbt.com/), a registered trademark of [Fishtown Analytics](https://www.fishtownanalytics.com/) From 22a2385d6d11ddaf83bfc7253187ddec3cf40573 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 4 Oct 2019 11:08:38 +0100 Subject: [PATCH 059/157] Update changelog.md --- docs/changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index effdc1e6d..9fc6093bd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.1pre] - 2019-09 / 2019-10 +## [v0.1-pre] - 2019-09 / 2019-10 ### Added - Table Macros: @@ -24,4 +24,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Documentation -- Numerous changes leading up to Version 1.0 release \ No newline at end of file +- Numerous changes leading up to Version 1.0 release From 97adb0af114bffc0529791126ba0dbcea6aed5b1 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 4 Oct 2019 11:20:08 +0100 Subject: [PATCH 060/157] Added latest/stable info --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 737fa0847..3ea2c5fb4 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,14 @@

+latest + [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) +stable + +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.1-pre)](https://dbtvault.readthedocs.io/en/v0.1-pre/?badge=v0.1-pre) + # dbtvault by [Datavault](https://www.data-vault.co.uk) dbtvault is a dbt package that generates & executes the ETL you need to run a Data Vault 2.0 Data Warehouse on a Snowflake database; @@ -21,10 +27,11 @@ powered by [dbt](https://www.getdbt.com/), a registered trademark of [Fishtown A Add the following to your ```packages.yml``` -```bash +```yaml packages: - git: "https://github.com/Datavault-UK/dbtvault" + revision 0.1pre # Latest stable version ``` And run ```dbt deps``` From 2aaae55979a81bd9c35fd077ca286aa65ca09194 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 4 Oct 2019 11:20:37 +0100 Subject: [PATCH 061/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ea2c5fb4..42fdced74 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision 0.1pre # Latest stable version + revision: v0.1-pre # Latest stable version ``` And run ```dbt deps``` From 58fffd3fe9c8d6737c6583772a764d29613fae67 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 4 Oct 2019 11:36:23 +0100 Subject: [PATCH 062/157] Update README.md --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 42fdced74..c640b8191 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,9 @@

-latest +latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) - -stable - -[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.1-pre)](https://dbtvault.readthedocs.io/en/v0.1-pre/?badge=v0.1-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.1-pre)](https://dbtvault.readthedocs.io/en/v0.1-pre/?badge=v0.1-pre) # dbtvault by [Datavault](https://www.data-vault.co.uk) From 25ec5d26e086f312995d427454f7316173649fd0 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 7 Oct 2019 16:55:23 +0100 Subject: [PATCH 063/157] Version v0.2-pre ## [v0.2-pre] - 2019-10-07 Improved Read the linked documentation for more detail on how to take advantage of the new and improved features. - Table Macros: - All table macros now no longer require the tgt_cols parameter. This was unnecessary duplication of metadata and removing this now makes creating tables much simpler. - Supporting Macros: - add_columns - Simplified the process of adding constants. - Can now optionally provide a dbt source to automatically retrieve all source columns without needing to type them all manually. - If not adding any calculated columns or constants, column pairs can be omitted, enabling you to provide the source parameter above only. - hash now alpha-sorts columns prior to hashing, as per best practises. - Staging Macros: - staging_footer renamed to from and functionality for adding constants moved to add_columns - multi-hash - Formatting of output now more readable - Now alpha-sorts columns prior to hashing, as per best practises. --- docs/changelog.md | 32 ++++ docs/gettingstarted.md | 39 ++++- docs/hubs.md | 45 +++--- docs/links.md | 33 ++-- docs/macros.md | 136 ++++++++-------- docs/roadmap.md | 13 +- docs/staging.md | 150 ++++++++++-------- macros/internal/create_source.sql | 7 +- .../get_tgt_cols.sql} | 31 +++- macros/internal/single.sql | 2 +- macros/internal/union.sql | 13 +- macros/staging/add_columns.sql | 40 ++++- .../create_col.sql => staging/from.sql} | 4 +- macros/staging/multi_hash.sql | 7 +- macros/supporting/hash.sql | 24 +-- macros/tables/hub_template.sql | 8 +- macros/tables/link_template.sql | 8 +- macros/tables/sat_template.sql | 4 +- 18 files changed, 365 insertions(+), 231 deletions(-) rename macros/{staging/staging_footer.sql => internal/get_tgt_cols.sql} (52%) rename macros/{internal/create_col.sql => staging/from.sql} (90%) diff --git a/docs/changelog.md b/docs/changelog.md index 9fc6093bd..8fc115dc9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v0.2-pre] - 2019-10-07 + +[Feedback is welcome!](https://github.com/Datavault-UK/dbtvault/issues) + +### Improved +Read the linked documentation for more detail on how to take advantage of +the new and improved features. + +- Table Macros: + - All table macros now no longer require the ```tgt_cols``` parameter. + This was unnecessary duplication of metadata and removing this now makes + creating tables much simpler. + +- Supporting Macros: + - [add_columns](macros.md#add_columns) + - Simplified the process of adding constants. + - Can now optionally provide a [dbt source](https://docs.getdbt.com/docs/using-sources) to automatically + retrieve all source columns without needing to type them all manually. + - If not adding any calculated columns or constants, column pairs can be omitted, enabling you to provide the + source parameter above only. + - [hash](macros.md#hash) now alpha-sorts columns prior to hashing, as + per best practises. + +- Staging Macros: + - staging_footer renamed to [from](macros.md#from) and functionality for adding constants moved to + [add_columns](macros.md#add_columns) + - [multi-hash](macros.md#multi_hash) + - Formatting of output now more readable + - Now alpha-sorts columns prior to hashing, as + per best practises. + ## [v0.1-pre] - 2019-09 / 2019-10 ### Added diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index b3f7c8ea0..7a5bcbae7 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -20,11 +20,43 @@ Happy Data Vaulting! :smile: 3. You must have downloaded and installed dbt, and [set up a project](https://docs.getdbt.com/docs/dbt-projects). -4. We assume you already have a raw staging layer. +4. Sources should be set up in dbt [(see below)](#setting-up-sources). -5. Our macros assume that you are only loading from one set of load dates in a single load cycle (i.e. Your staging layer +5. We assume you already have a raw staging layer. + +6. Our macros assume that you are only loading from one set of load dates in a single load cycle (i.e. Your staging layer contains data for one ```load_datetime``` value only). **We will be removing this restriction in future releases**. + + +## Setting up sources + +We will be using the ```source``` feature of dbt extensively throughout the documentation to make access to source +data much easier, cleaner and more modular. The main advantage of this is that sources will be included in +dbt dependency graphs + +We have provided an example below which shows a configuration similar to that used for the examples in our documentation, +however this feature is documented extensively in dbts own documentation, +so please [read here](https://docs.getdbt.com/docs/using-sources). + +After reading the above documentation, we recommend you place the ```schema.yml``` file you create for your sources, +in the root of your ```models``` folder, however you can place it where needed for your specific project. + +```schema.yml``` + +```yaml +version: 2 + +sources: + - name: MYSOURCE + database: MYDATABASE + schema: MYSCHEMA + tables: + - name: stg_customer + identifier: table_1 + - name: ... +``` + ## Installation Add the following to your ```packages.yml```: @@ -37,4 +69,5 @@ packages: And run ```dbt deps``` -###### [Read more on package installation (from dbt)](https://docs.getdbt.com/docs/package-management) \ No newline at end of file +[Read more on package installation (from dbt)](https://docs.getdbt.com/docs/package-management) + diff --git a/docs/hubs.md b/docs/hubs.md index b3440c731..ec7c47fe2 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -13,7 +13,7 @@ order number (can be multi-column). ### Creating the model header -Create a new dbt model as before. We'll call this one 'hub_customer'. +Create a new dbt model as before. We'll call this one ```hub_customer```. The following header is what we use, but feel free to customise it to your needs: @@ -70,7 +70,7 @@ provide the metadata it requires. We can define which source columns map to the define a column type at the same time: ```hub_customer.sql``` -```sql hl_lines="8 10 11 12 13" +```sql hl_lines="8 9 10 11" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} {%- set src_pk = 'CUSTOMER_PK' -%} @@ -78,35 +78,26 @@ define a column type at the same time: {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_cols = [src_pk, src_nk, src_ldts, src_source] -%} - {%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} {%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} {%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} {%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} ``` -With these 5 additional lines, we have now informed the macro how to transform our source data: +With these 4 additional lines, we have now informed the macro how to transform our source data: -- On line 8, we have written the 4 columns we worked out earlier to define what source columns -we are using and what order we want them in. We have used the variable names to avoid writing the columns again. -- On the remaining lines we have provided our mapping from source to target. In this particular scenario we aren't +- We have provided our mapping from source to target. In this particular scenario we aren't renaming the columns, so we have used the source column reference on both sides. If you need to rename the columns however, this feature allows you to do so. -- We have provided a type in the mapping so that the type is explicitly defined. For now, this is not optional, but we -will simplify this for scenarios where we want the data type or column name to remain unchanged in future releases. + +- We have provided a type in the mapping so that the type is explicitly defined. For now, this is not optional, but +in future releases we will simplify this for scenarios where we want the data type or column name to remain unchanged. !!! info There is nothing to stop you entering invalid type mappings in this step (i.e. trying to cast an invalid date format to a date), so please ensure they are correct. You will soon find out, however, as dbt will issue a warning to you. No harm done, but save time by providing accurate metadata! - - -!!! question "Why is ```tgt_cols``` needed?" - In future releases, we will eliminate the need to duplicate the source columns as shown on line 8. - - For now, this is a necessary evil. #### Source table @@ -118,7 +109,7 @@ dbt ensures dependencies are honoured when defining the source using a reference ```hub_customer.sql``` -```sql hl_lines="15" +```sql hl_lines="13" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} {%- set src_pk = 'CUSTOMER_PK' -%} @@ -126,8 +117,6 @@ dbt ensures dependencies are honoured when defining the source using a reference {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_cols = [src_pk, src_nk, src_ldts, src_source] -%} - {%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} {%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} {%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} @@ -136,13 +125,17 @@ dbt ensures dependencies are honoured when defining the source using a reference {%- set source = [ref('stg_orders_hashed')] -%} ``` +!!! note + Make sure you surround the ref call with square brackets, as shown in the snippet + above. + ### Invoking the template Now we bring it all together and call the [hub_template](macros.md#hub_template) macro: ```hub_customer.sql``` -```sql hl_lines="17 18 19" +```sql hl_lines="15 16 17" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} {%- set src_pk = 'CUSTOMER_PK' -%} @@ -150,19 +143,17 @@ Now we bring it all together and call the [hub_template](macros.md#hub_template) {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_cols = [src_pk, src_nk, src_ldts, src_source] -%} - {%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} {%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} {%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} {%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} -{%- set source = [ref('stg_customer_hashed')] -%} +{%- set source = [ref('stg_orders_hashed')] -%} {{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, source) }} -``` +``` ### Running dbt @@ -171,8 +162,8 @@ With our model complete, we can run dbt to create our ```hub_customer``` hub. ```dbt run --models +hub_customer``` !!! tip - The '+' in the command above will cause dbt to also compile and run all parent dependencies for the model we are - running, in this case, it will re-create the staging layer from the ```stg_customer_hashed``` model if needed. + Using the '+' in the command above will get dbt to compile and run all parent dependencies for the model we are + running, in this case, it will re-create the staging layer from the ```stg_orders_hashed``` model if needed. dbt will also create our hub if it doesn't already exist. And our table will look like this: diff --git a/docs/links.md b/docs/links.md index 2986b1c0f..df3b50512 100644 --- a/docs/links.md +++ b/docs/links.md @@ -18,7 +18,7 @@ referenced) ### Creating the model header -Create another empty dbt model. We'll call this one 'link_customer_nation'. +Create another empty dbt model. We'll call this one ```link_customer_nation```. The following header is what we use, but feel free to customise it to your needs: @@ -68,7 +68,7 @@ provide the metadata it requires. We can define which source columns map to the define a column type at the same time: ```link_customer_nation.sql``` -```sql hl_lines="8 10 11 12 14 15" +```sql hl_lines="8 9 10 11 12 13" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} {%- set src_pk = 'CUSTOMER_NATION_PK' -%} @@ -76,8 +76,6 @@ define a column type at the same time: {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', src_ldts, src_source] -%} - {%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} {%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} @@ -87,13 +85,9 @@ define a column type at the same time: ``` -With these 5 additional lines, we have now informed the macro how to transform our source data: - -- On line 8, we have written the 5 columns we worked out earlier to define what source columns -we are using and what order we want them in. The column name strings on lines 8 and 11 could easily be replaced with -references to the ```src_pk``` and ```src_fk``` variables, these are just written in full for clarity. +With these 4 additional lines, we have now informed the macro how to transform our source data: -- On the remaining lines we have provided our mapping from source to target. Observe that we are +- We have provided our mapping from source to target. Observe that we are renaming the foreign key column so that they have an ```FK``` suffix. - We have provided a type in the mapping so that the type is explicitly defined. For now, this is not optional, but we @@ -104,11 +98,6 @@ will simplify this for scenarios where we want the data type or column name to r so please ensure they are correct. You will soon find out, however, as dbt will issue a warning to you. No harm done, but save time by providing accurate metadata! - -!!! question "Why is ```tgt_cols``` needed?" - In future releases, we will eliminate the need to duplicate the source columns as shown on line 8. - - For now, this is a necessary evil. #### Source table @@ -117,7 +106,7 @@ the staging layer model we made earlier, as this contains all the columns we nee ```link_customer_nation.sql``` -```sql hl_lines="17" +```sql hl_lines="15" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} {%- set src_pk = 'CUSTOMER_NATION_PK' -%} @@ -125,8 +114,6 @@ the staging layer model we made earlier, as this contains all the columns we nee {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', src_ldts, src_source] -%} - {%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} {%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} @@ -137,12 +124,16 @@ the staging layer model we made earlier, as this contains all the columns we nee {%- set source = [ref('stg_orders_hashed')] -%} ``` +!!! note + Make sure you surround the ref call with square brackets, as shown in the snippet + above. + ### Invoking the template Now we bring it all together and call the [link_template](macros.md#link_template) macro: ```link_customer_nation.sql``` -```sql hl_lines="19 20 21" +```sql hl_lines="17 18 19" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} {%- set src_pk = 'CUSTOMER_NATION_PK' -%} @@ -150,8 +141,6 @@ Now we bring it all together and call the [link_template](macros.md#link_templat {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', src_ldts, src_source] -%} - {%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} {%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} @@ -162,7 +151,7 @@ Now we bring it all together and call the [link_template](macros.md#link_templat {%- set source = [ref('stg_orders_hashed')] -%} {{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, source) }} ``` diff --git a/docs/macros.md b/docs/macros.md index 3da5d484d..c8318844b 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -10,7 +10,7 @@ Creates a hub with provided metadata. ```mysql dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, source) ``` @@ -22,7 +22,6 @@ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, | src_nk | Source natural key column | String | List | check_circle | | src_ldts | Source loaddate timestamp column | String | String | check_circle | | src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_cols | Complete list of all source columns (pre-aliasing) | List | List | check_circle | | tgt_pk | Target primary key column | List | List | check_circle | | tgt_nk | Target natural key column | List | List | check_circle | | tgt_ldts | Target loaddate timestamp column | List | List | check_circle | @@ -40,9 +39,7 @@ hub_customer.sql: {%- set src_pk = 'CUSTOMER_PK' -%} {%- set src_nk = 'CUSTOMER_ID' -%} {%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_cols = [src_pk, src_nk, src_ldts, src_source] -%} +{%- set src_source = 'SOURCE' -%} {%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} {%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} @@ -52,7 +49,7 @@ hub_customer.sql: {%- set source = [ref('stg_customer_hashed')] -%} {{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, source) }} ``` @@ -67,8 +64,6 @@ hub_parts.sql: {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_cols = [src_pk[0], src_nk[0], src_ldts, src_source] -%} - {%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} {%- set tgt_nk = [src_nk[0], 'NUMBER(38,0)', src_nk[0]] -%} {%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} @@ -80,7 +75,7 @@ hub_parts.sql: {{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, source) }} ``` @@ -109,7 +104,7 @@ SELECT DISTINCT CAST(stg.LOADDATE AS DATE) AS LOADDATE, CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE FROM ( - SELECT PART_PK, PART_ID, LOADDATE, SOURCE, + SELECT src.PART_PK, src.PART_ID, src.LOADDATE, src.SOURCE, LAG(SOURCE, 1) OVER(PARTITION by PART_PK ORDER BY PART_PK) AS FIRST_SOURCE @@ -121,7 +116,8 @@ FROM ( FROM MYDATABASE.MYSCHEMA.stg_supplier_hashed AS b UNION SELECT c.PART_PK, c.PART_ID, c.LOADDATE, c.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_lineitem_hashed AS c) + FROM MYDATABASE.MYSCHEMA.stg_lineitem_hashed AS c + ) as src ) AS stg LEFT JOIN MYDATABASE.MYSCHEMA.hub_parts AS tgt ON stg.PART_PK = tgt.PART_PK @@ -137,7 +133,7 @@ Creates a link with provided metadata. ```mysql dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, source) ``` @@ -149,7 +145,6 @@ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, | src_fk | Source foreign key column | List | List | check_circle | | src_ldts | Source loaddate timestamp column | String | String | check_circle | | src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_cols | Complete list of all source columns (pre-aliasing) | List | List | check_circle | | tgt_pk | Target primary key column | List | List | check_circle | | tgt_fk | Target foreign key column | List | List | check_circle | | tgt_ldts | Target loaddate timestamp column | List | List | check_circle | @@ -169,9 +164,6 @@ link_customer_nation.sql: {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', - src_ldts, src_source] -%} - {%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} {%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} @@ -182,7 +174,7 @@ link_customer_nation.sql: {%- set source = [ref('stg_crm_customer_hashed')] -%} {{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, source) }} ``` @@ -200,9 +192,6 @@ link_customer_nation_union.sql: {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_cols = ['CUSTOMER_NATION_PK', 'CUSTOMER_PK', 'NATION_PK', - src_ldts, src_source] -%} - {%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} {%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} @@ -215,7 +204,7 @@ link_customer_nation_union.sql: ref('stg_web_customer_hashed')] -%} {{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, source) }} ``` @@ -246,7 +235,7 @@ SELECT DISTINCT CAST(stg.LOADDATE AS DATE) AS LOADDATE, CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE FROM ( - SELECT CUSTOMER_NATION_PK, CUSTOMER_PK, NATION_PK, LOADDATE, SOURCE, + SELECT src.CUSTOMER_NATION_PK, src.CUSTOMER_PK, src.NATION_PK, src.LOADDATE, src.SOURCE, LAG(SOURCE, 1) OVER(PARTITION by CUSTOMER_NATION_PK ORDER BY CUSTOMER_NATION_PK) AS FIRST_SOURCE @@ -258,7 +247,8 @@ FROM ( FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS b UNION SELECT c.CUSTOMER_NATION_PK, c.CUSTOMER_PK, c.NATION_PK, c.LOADDATE, c.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_web_customer_hashed AS c) + FROM MYDATABASE.MYSCHEMA.stg_web_customer_hashed AS c + ) AS src ) AS stg LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation_union AS tgt ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK @@ -275,7 +265,7 @@ Creates a satellite with provided metadata. ```mysql dbtvault.sat_template(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, + tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source, src_table, source) ``` @@ -290,7 +280,6 @@ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, | src_eff | Source effective from column | String | check_circle | | src_ldts | Source loaddate timestamp column | String | check_circle | | src_source | Name of the column containing the source ID | String | check_circle | -| tgt_cols | Complete list of all source columns (pre-aliasing) | List | check_circle | | tgt_pk | Target primary key column | List | check_circle | | tgt_hashdiff | Target hashdiff column | List | check_circle | | tgt_payload | Target payload column | List | check_circle | @@ -316,8 +305,6 @@ sat_customer_details.sql: {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_cols = [src_pk, 'HASHDIFF', 'NAME', 'DOB', 'PHONE', 'EFFECTIVE_FROM', 'LOADDATE', 'SOURCE'] -%} - {%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} {%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} @@ -334,7 +321,7 @@ sat_customer_details.sql: {{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, + tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source, source) }} ``` @@ -378,7 +365,7 @@ ___ ## Staging Macros ######(macros/staging) -These macros are intended for use in the staging layer +These macros are intended for use in the staging layer. ___ ### multi_hash @@ -412,20 +399,24 @@ CAST(MD5_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(16)) AS alias2 ```yaml {{ dbtvault.multi_hash([('CUSTOMERKEY', 'CUSTOMER_PK'), - (['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF')]) }} + (['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF')]) }} ``` #### Output ```mysql CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, -CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) - AS BINARY(16)) AS HASHDIFF + +CAST(MD5_BINARY(CONCAT( + IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) AS BINARY(16)) AS HASHDIFF ``` +!!! success "Column sorting" + You do not need to worry about providing the columns in any particular order; Provided columns are alpha-sorted automatically, as per best practises. + ___ ### add_columns @@ -437,64 +428,84 @@ column AS alias #### Parameters -| Parameter | Description | Type | Required? | -| ------------- | ----------------------------------- | -------------- | ------------------------------------------------------------------ | -| pairs | Collection of (column, alias) pairs | List of tuples | check_circle | +| Parameter | Description | Type | Required? | +| ------------- | ----------------------------------- | -------------- | ----------------------------------------------- | +| source_table | A source reference | Source | clear | +| pairs | List of (column, alias) pairs | List of tuples | clear | + +!!! note + At least one of the above parameters must be provided, both may be provided if required. #### Usage ```yaml -{{ dbtvault.add_columns([('PARTKEY', 'PART_ID'), - ('PART_NAME', 'NAME'), - ('PART_TYPE', 'TYPE'), - ('PART_SIZE', 'SIZE'), - ('PART_RETAILPRICE', 'RETAILPRICE'), - ('LOADDATE', 'LOADDATE'), - ('SOURCE', 'SOURCE')]) }} +{{ dbtvault.add_columns(source('MYSOURCE', 'MYTABLE'), + [('CURRENT_DATE()', 'EFFECTIVE_FROM'), + ('!STG_CUSTOMER', 'SOURCE')]) }} ``` #### Output ```mysql -PARTKEY AS PART_ID, -PART_NAME AS NAME, -PART_TYPE AS TYPE, -PART_SIZE AS SIZE, -PART_RETAILPRICE AS RETAILPRICE, -LOADDATE AS LOADDATE, -SOURCE AS SOURCE +, +CURRENT_DATE() AS EFFECTIVE_FROM, +'STG_CUSTOMER' AS SOURCE +``` + +#### Notes + +##### Adding constants +With the ```add_columns``` macro, you may provide constants. +These are additional 'calculated' columns created from hard-coded values. +To achieve this, simply provide the constant with a ```!``` in front of the desired constant, +and the macro will do the rest. See line 3 above, and the output it gives. + + +##### Getting columns from the source +The ```add_columns``` macro will automatically select all columns from the provided source reference. +If you need to override any of these columns and provide different values, for example a different ```SOURCE``` +or ```LOADDATE``` column value, then you may. If you provide columns in the ```pairs``` parameter, then they will +automatically take precedence over any columns coming from the source. + +Database functions may be used, for example ```CURRENT_DATE()```, to set the current date as the value of a column, as in the +example below: + +```sql +{{ dbtvault.add_columns(source_table, + [('!TPCH', 'SOURCE'), + ('CURRENT_DATE()', 'EFFECTIVE_FROM')]) }} ``` + ___ -### staging_footer +### from Used in creating source/hashing models to complete a staging layer model. ```mysql -,LOADDATE AS LOADDATE,'SOURCE' AS SOURCE FROM DV_PROTOTYPE_DB.SRC_TEST_STG.test_stg_lineitem +FROM MYDATABASE.MYSCHEMA.MYTABLE ``` +!!! info + Sources need to be set up in dbt. [Read More](https://docs.getdbt.com/docs/using-sources) + #### Parameters | Parameter | Description | Type | Required? | | ------------- | ----------------------------------------- | ------ | -------------------------------------------------------- | -| loaddate | Name for loaddate column | String | clear | -| source | Source column value for each record | String | clear | -| source_table | Fully qualified table name | String | check_circle | +| source_table | A source reference | Source | check_circle | #### Usage ```yaml -{{- dbtvault.staging_footer('LOADDATE', - 'SOURCE', - source_table='MYDATABASE.MYSCHEMA.MYTABLE') }} +{{ dbtvault.from( source('MYSOURCE', 'MYTABLE') ) }} ``` #### Output ```mysql -,LOADDATE AS LOADDATE,'SOURCE' AS SOURCE FROM MYDATABASE.MYSCHEMA.MYTABLE +FROM MYDATABASE.MYSCHEMA.MYTABLE ``` ___ @@ -559,7 +570,7 @@ ___ ### hash !!! warning - This macro ***should not be*** used for cryptographic purposes + This macro ***should not be*** used for cryptographic purposes. The intended use is for creating checksum-like fields only, so that a record change can be detected. @@ -571,6 +582,7 @@ CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias ``` - Can provide multiple columns as a list to create a concatenated hash +- When multiple columns are provided, they are alpha-sorted automatically. - Casts a column as ```VARCHAR```, transforms to ```UPPER``` case and trims whitespace - ```'^^'``` Accounts for null values with a double caret - ```'||'``` Concatenates with a double pipe diff --git a/docs/roadmap.md b/docs/roadmap.md index 097692cc4..5b8b5ed93 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -33,10 +33,15 @@ Release 1 will include: - prefix #### Planned improvements -- Easier staging: we're planning to reduce the steps and metadata required -- Removal of the need to add columns which already exist in raw staging, -and making it so ```add_columns``` is only required for calculated columns or other user-defined additions. -- Removal of the ```tgt_cols``` parameter in the table templates, as this is duplication of metadata. + +- Make providing aliases and types optional when defining target metadata in table template macros. + +!!! success "New in v0.2-pre:" + - Removed the need to add columns which already exist in raw staging. + [add_columns](macros.md#add_columns) is now only requires entry of metadata for calculated columns or other user-defined additions. + - Removed of the ```tgt_cols``` parameter in the table templates, as this was duplication of metadata. + - Hashing now alpha-sorts columns automatically. + ## Release 2.0 #### Tables diff --git a/docs/staging.md b/docs/staging.md index a44af93ec..ba44f33a7 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -1,4 +1,3 @@ - ![alt text](./assets/images/staging.png "Staging from a raw table to the raw vault") The dbtvault package assumes you've already loaded a Snowflake database staging table with raw data @@ -12,6 +11,12 @@ There are a few conditions that need to be met for the dbtvault package to work: The raw staging table needs to be pre processed to add extra columns of data to make it ready to load to the raw vault. Specifically, we need to add primary key hashes, hashdiffs, and any implied fixed-value columns (see the diagram). +!!! info + - Hashing of primary keys is optional in Snowflake + - Natural keys alone can be used + - We've implemented hashing as the only option, for now + - A non-hashed version will be added in future releases + ### Creating the model header First we create a new dbt model. Our source table is called ```stg_customer``` @@ -38,6 +43,21 @@ our schema name: Usually we want hashing layers to be views. - The ```schema``` parameter is the name of the schema where this staging table will be created. +### Setting the source table + +Next we will create a variable which holds a reference to the raw source table, since we will need to refer to it a few times +in our model. + +```stg_orders_hashed.sql``` +```sql hl_lines="3" + +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} + +{%- set source_table = source('MYSOURCE', 'stg_customer') -%} +``` + + + ### Adding the metadata Now we get into the core component of staging: the metadata. @@ -51,9 +71,11 @@ provided in the link above. After adding the macro call, our model will now look something like this: ```stg_orders_hashed.sql``` -```sql hl_lines="3 4 5" +```sql hl_lines="5 6 7" {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} + +{%- set source_table = source('MYSOURCE', 'stg_customer') -%} {{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), ('NATION_ID', 'NATION_PK'), @@ -61,7 +83,7 @@ After adding the macro call, our model will now look something like this: ``` !!! note - Make sure you add the trailing comma after the call, at the end of line 5. + Make sure you add the trailing comma after the call, at the end of line 7. This call will: @@ -76,88 +98,88 @@ The latter two pairs will be used later when creating [links](links.md). ### Additional columns +With the [add_columns](macros.md#add_columns) macro, we can provide a list of columns and any corresponding aliases for +those columns. + We now add the column names we want to bring forward/feed from the raw staging table into the raw vault. -We list them by name, and provide an alias (how we want to name them in the raw vault tables). You will also -have another opportunity to rename these columns later when creating the raw vault tables. +To include all columns which exist in the source table, we provide the ```source_table``` variable we created earlier. -We will need to add some additional columns to our staging layer, containing 'constants' implied by the context of the +We will also need to add some additional columns to our staging layer, containing 'constants' implied by the context of the staging data. For example, we may add a source table code value, or the the load date, or some other constant needed in -the primary key. Load dates and sources are also handled in the next section, as you have a choice of techniques. +the primary key. -With the [add_columns](macros.md#add_columns) macro, we can provide a list of columns and any corresponding aliases for -those columns. +We can also override any columns coming in from the source, with different data. We may want to do this if a source +column already exists in the raw stage and the values aren't appropriate. + +We provide the constant by adding a ```!``` to the data and alias them with the same name as the column we want to +override. You will have another opportunity to rename these columns, as well as cast them to different data types +later when creating the raw vault tables. We can also use this method to create any new columns which do not already +exist in the source. ```stg_orders_hashed.sql``` -```sql hl_lines="7 8 9 10 11 12" +```sql hl_lines="9 10 11" {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} + +{%- set source_table = source('MYSOURCE', 'stg_customer') -%} {{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), ('NATION_ID', 'NATION_PK'), (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK')]) -}}, -{{ dbtvault.add_columns([('CUSTOMER_ID', 'CUSTOMER_ID'), - ('NATION_ID', 'NATION_ID'), - ('CUSTOMER_DOB', 'CUSTOMER_DOB'), - ('CUSTOMER_NAME', 'CUSTOMER_NAME'), - ('LOADDATE', 'LOADDATE'), +{{ dbtvault.add_columns(source_table, + [('!1', 'SOURCE'), ('LOADDATE', 'EFFECTIVE_FROM')]) }} ``` -!!! note - In future releases, this step won't be necessary for adding columns which already exist in the raw stage, - as dbtvault will automatically include the rest of the columns found in our staging table for us. +!!! success "New" + We are now no longer required to provide columns which already exist in the source table, + as providing the ```source_table``` parameter in ```add_columns``` will now bring in all the columns + for us. + -In the example above we have have a header, have defined some hashing to create primary keys, and added 6 columns: -5 from the raw stage table, and an added value (```EFFECTIVE_FROM```). +In the example above we have have: + +- Added a header (line 1). +- Set the source_table variable to our raw staging table (line 3). +- Defined some hashing to create primary keys (lines 5-7). +- Brought in all of the raw staging table's columns (line 9). +- Added a ```SOURCE``` column with the constant value ```1``` (line 10). +- Added an ```EFFECTIVE_FROM``` column which uses the ```LOADDATE``` value as its value (line 11). ### Adding the footer -Finally, we need to provide a fully qualified source table name for our staging layer SQL to get data from. -In this example, this would be ```MYDATABASE.MYSCHEMA.stg_customer``` where ```MYDATABASE.MYSCHEMA``` is the -database and schema in your Snowflake database where your raw staging table resides. - -The [staging_footer](macros.md#staging_footer) macro generates the SQL using the metadata. +!!! success "New" + The ```staging_footer``` macro has been renamed to ```from``` and is now much simpler. + If you're looking for the ability to add constants for ```source``` and ```loaddate```, + you can now use the improved [add_columns](macros.md#add_columns) macro. + -As explained in the [documentation](macros.md#staging_footer), this macro also has ```loaddate``` and ```source``` parameters. -These are to simplify the creation of ```SOURCE``` and ```LOADDATE``` columns. - The parameters can be omitted in favour of adding them via the [add_columns](macros.md#add_columns) macro, - as showcased in the snippet above with ```LOADDATE```. +Now we just need to provide the variable we created earlier, as a parameter to the [from](macros.md#from) +macro. After adding the footer, our completed model should now look like this: - ```stg_orders_hashed.sql``` -```sql hl_lines="14 15" +```sql hl_lines="13" -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} - -{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), - ('NATION_ID', 'NATION_PK'), - (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK')]) -}}, - -{{ dbtvault.add_columns([('CUSTOMER_ID', 'CUSTOMER_ID'), - ('NATION_ID', 'NATION_ID'), - ('CUSTOMER_DOB', 'CUSTOMER_DOB'), - ('CUSTOMER_NAME', 'CUSTOMER_NAME'), - ('LOADDATE', 'LOADDATE'), - ('LOADDATE', 'EFFECTIVE_FROM')]) }} - -{{- dbtvault.staging_footer(source="1", - source_table='MYDATABASE.MYSCHEMA.stg_customer') }} +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} -``` +{%- set source_table = source('MYSOURCE', 'stg_customer') -%} + +{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), + ('NATION_ID', 'NATION_PK'), + (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK')]) -}}, -!!! tip - In the call to [staging_footer](macros.md#staging_footer) we have provided ```1``` as the value for the - ```source``` parameter, this will give every record in our new staging layer the value - ```1``` as its ```SOURCE```. This is a code which can be used to find the source in a lookup table. - This will allow us to trace this data back to the source once it is loaded into our vault from our new staging layer. - - It is entirely optional, and if you already have a source column in your raw staging, you can simply add it using - [add_columns](macros.md#add_columns) instead. +{{ dbtvault.add_columns(source_table, + [('LOADDATE', 'EFFECTIVE_FROM'), + ('!1', 'SOURCE')]) }} + +{{ dbtvault.from(source_table) }} + +``` This model is now ready to run to create a view with all the added data/columns needed to load the raw vault. @@ -169,18 +191,12 @@ With our model complete, we can run dbt and have our new staging layer materiali And our table will look like this: -| CUSTOMER_PK | NATION_PK | CUSTOMER_NATION_PK | CUSTOMER_ID | NATION_ID | CUSTOMER_DOB | CUSTOMER_NAME | LOADDATE | EFFECTIVE_FROM | SOURCE | -| ------------ | ------------ | ------------ | ------------ | ------------ | ------------- | -------------- | ---------- | -------------- | ------------ | -| B8C37E... | D89F3A... | 72A160... | 1001 | 10001 | 1997-04-24 | Alice | 1993-01-01 | 1993-01-01 | 1 | -| . | . | . | . | . | . | . | . | . | . | -| . | . | . | . | . | . | . | . | . | . | -| FED333... | D78382... | 1CE6A9... | 1004 | 10004 | 2018-04-13 | Dom | 1993-01-01 | 1993-01-01 | 1 | - -!!! info - - Hashing of primary keys is optional in Snowflake - - Natural keys alone can be used - - We've implemented hashing as the only option, for now - - A non-hashed version will be added in future releases +| CUSTOMER_PK | NATION_PK | CUSTOMER_NATION_PK | (source table columns) | EFFECTIVE_FROM | SOURCE | +| ------------ | ------------ | ------------------- | ---------------------- | -------------- | ------------ | +| B8C37E... | D89F3A... | 72A160... | . | 1993-01-01 | 1 | +| . | . | . | . | . | . | +| . | . | . | . | . | . | +| FED333... | D78382... | 1CE6A9... | . | 1993-01-01 | 1 | ### Next steps diff --git a/macros/internal/create_source.sql b/macros/internal/create_source.sql index b9486794a..5f258143e 100644 --- a/macros/internal/create_source.sql +++ b/macros/internal/create_source.sql @@ -12,16 +12,17 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro create_source(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk, +{%- macro create_source(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, source, is_union) -%} {%- if not is_union -%} - {{- dbtvault.single(src_pk, src_nk, src_ldts, src_source, tgt_pk, source[0], 'a') -}} + {{- dbtvault.single(src_pk, src_nk, src_ldts, src_source, source[0], 'a') -}} {%- else -%} - {{- dbtvault.union(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk, source) -}} + {{- dbtvault.union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -}} {%- endif -%} diff --git a/macros/staging/staging_footer.sql b/macros/internal/get_tgt_cols.sql similarity index 52% rename from macros/staging/staging_footer.sql rename to macros/internal/get_tgt_cols.sql index fc0a8c91a..11cf8ce34 100644 --- a/macros/staging/staging_footer.sql +++ b/macros/internal/get_tgt_cols.sql @@ -12,8 +12,33 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro staging_footer(loaddate, source, source_table) -%} -{%- if source or loaddate -%}, {%- endif -%} -{%- if loaddate -%} {{ loaddate }} AS LOADDATE, {%- endif -%} {%- if source -%} '{{ source }}' AS SOURCE {%- endif %} FROM {{ source_table }} +{%- macro get_tgt_cols(tgt_cols) -%} + +{%- set col_list = [] -%} + +{%- if tgt_cols is iterable -%} + + {%- for col_set in tgt_cols -%} + + {#- If a triple -#} + {%- if col_set | first is string -%} + + {%- set _ = col_list.append(col_set|last) -%} + + {#- If list of lists -#} + {%- elif col_set is iterable and col_set is not string -%} + + {%- for cols in col_set -%} + + {%- set _ = col_list.append(cols|last) -%} + + {%- endfor -%} + + {%- endif -%} + + {%- endfor -%} +{%- endif -%} + +{{ return(col_list) }} {%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/single.sql b/macros/internal/single.sql index c27619374..373ab7fd3 100644 --- a/macros/internal/single.sql +++ b/macros/internal/single.sql @@ -12,7 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro single(src_pk, src_nk, src_ldts, src_source, tgt_pk, +{%- macro single(src_pk, src_nk, src_ldts, src_source, source, letter='a') -%} SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], letter) }} diff --git a/macros/internal/union.sql b/macros/internal/union.sql index bc254e5b9..0d6cd4c95 100644 --- a/macros/internal/union.sql +++ b/macros/internal/union.sql @@ -12,12 +12,12 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro union(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk, source) -%} +{%- macro union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -%} - SELECT {{ tgt_cols|join(", ") }}{% if is_incremental() or union -%}, + SELECT {{ dbtvault.prefix([src_pk[0], src_nk[0], src_ldts, src_source], 'src')}}{% if is_incremental() or union -%}, LAG({{ src_source }}, 1) - OVER(PARTITION by {{ tgt_pk }} - ORDER BY {{ tgt_pk }}) AS FIRST_SOURCE + OVER(PARTITION by {{ tgt_pk | last }} + ORDER BY {{ tgt_pk | last }}) AS FIRST_SOURCE {%- endif %} FROM ( @@ -28,9 +28,10 @@ {%- for src in range(iterations) -%} {%- set letter = letters[loop.index0] %} {{ dbtvault.single(src_pk[loop.index0], src_nk[loop.index0], src_ldts, src_source, - tgt_pk, source[loop.index0], letter) -}} + source[loop.index0], letter) -}} {% if not loop.last %} UNION {%- endif -%} - {%- endfor -%}) + {%- endfor %} + ) AS src {%- endmacro -%} \ No newline at end of file diff --git a/macros/staging/add_columns.sql b/macros/staging/add_columns.sql index 201c4cbd8..408663034 100644 --- a/macros/staging/add_columns.sql +++ b/macros/staging/add_columns.sql @@ -12,11 +12,39 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro add_columns(pairs) -%} -{% for pair in pairs -%} +{%- macro add_columns(source, pairs=[]) -%} - {{ dbtvault.create_col(pair[0], pair[1]) }} - {%- if not loop.last -%} , {% endif %} -{% endfor %} +{%- set exclude_columns = [] -%} +{%- set include_columns = [] -%} -{%- endmacro -%} +{%- if source is defined and source is not none -%} +{%- set cols = adapter.get_columns_in_relation(source) -%} +{%- endif %} + +{#- Add aliases of provided pairs to excludes and full SQL to includes -#} +{%- for pair in pairs -%} + {%- if pair[0] | first == "!" -%} + {%- set _ = include_columns.append("'" ~ pair[0][1:] ~ "' AS " ~ pair[1]) -%} + {%- set _ = exclude_columns.append(pair[1]) -%} + {%- else -%} + {%- set _ = include_columns.append(pair[0] ~ " AS " ~ pair[1]) -%} + {%- set _ = exclude_columns.append(pair[1]) -%} + {%- endif %} +{%- endfor -%} + +{%- if source is defined and source is not none -%} +{#- Add all columns from source table -#} +{%- for col in cols -%} + {%- if col.column not in exclude_columns -%} + {%- set _ = include_columns.append(col.column) -%} + {%- endif -%} +{%- endfor -%} +{%- endif %} + +{#- Print out all columns in includes -#} +{%- for col in include_columns %} + {{ col }}{%if not loop.last %}, +{%- endif -%} + +{%- endfor -%} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/create_col.sql b/macros/staging/from.sql similarity index 90% rename from macros/internal/create_col.sql rename to macros/staging/from.sql index 50fac0f48..cb287b710 100644 --- a/macros/internal/create_col.sql +++ b/macros/staging/from.sql @@ -12,8 +12,8 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro create_col(column, alias) -%} +{% macro from(source_table) %} -{{ column }} AS {{ alias }} +FROM {{ source_table }} {%- endmacro -%} \ No newline at end of file diff --git a/macros/staging/multi_hash.sql b/macros/staging/multi_hash.sql index 892d60301..a76410f9c 100644 --- a/macros/staging/multi_hash.sql +++ b/macros/staging/multi_hash.sql @@ -16,9 +16,8 @@ -- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault SELECT {% for pair in pairs -%} - {{ dbtvault.hash(pair[0], pair[1]) }} - {%- if not loop.last -%} , {% endif %} -{% endfor %} - + {%- if not loop.last -%} , + {% endif %} +{%- endfor -%} {%- endmacro -%} diff --git a/macros/supporting/hash.sql b/macros/supporting/hash.sql index a75cd5968..707f57e06 100644 --- a/macros/supporting/hash.sql +++ b/macros/supporting/hash.sql @@ -14,22 +14,22 @@ -#} {%- macro hash(columns, alias) -%} -{%- if columns is string -%} +{#- Alpha sort columns before hashing -#} +{%- if columns is iterable and columns is not string -%} +{%- set columns = columns|sort -%} +{%- endif -%} -CAST(MD5_BINARY(UPPER(TRIM(CAST({{columns}} AS VARCHAR)))) AS BINARY(16)) AS {{alias}} +{%- if columns is string %} + CAST(MD5_BINARY(UPPER(TRIM(CAST({{columns}} AS VARCHAR)))) AS BINARY(16)) AS {{alias}} -{%- else -%} +{%- else %} -CAST(MD5_BINARY(CONCAT( - -{%- for column in columns[:-1] -%} - -IFNULL(UPPER(TRIM(CAST({{column}} AS VARCHAR))), '^^'), '||', - -{%- if loop.last -%} - -IFNULL(UPPER(TRIM(CAST({{columns[-1]}} AS VARCHAR))), '^^') )) AS BINARY(16)) AS {{alias}} + CAST(MD5_BINARY(CONCAT( +{%- for column in columns[:-1] %} + IFNULL(UPPER(TRIM(CAST({{column}} AS VARCHAR))), '^^'), '||', +{%- if loop.last %} + IFNULL(UPPER(TRIM(CAST({{columns[-1]}} AS VARCHAR))), '^^') )) AS BINARY(16)) AS {{alias}} {%- endif -%} {%- endfor -%} {%- endif -%} diff --git a/macros/tables/hub_template.sql b/macros/tables/hub_template.sql index e8c4f0a70..ca7fb7b48 100644 --- a/macros/tables/hub_template.sql +++ b/macros/tables/hub_template.sql @@ -13,14 +13,14 @@ limitations under the License. -#} {%- macro hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, source) -%} - -{%- set is_union = true if source|length > 1 else false -%} -- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault +{% set is_union = true if source|length > 1 else false %} SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_nk, tgt_ldts, tgt_source], 'stg') }} FROM ( - {{ dbtvault.create_source(src_pk, src_nk, src_ldts, src_source, tgt_cols, tgt_pk|last, + {{ dbtvault.create_source(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, source, is_union) }} ) AS stg {% if is_incremental() or is_union -%} diff --git a/macros/tables/link_template.sql b/macros/tables/link_template.sql index d3a1b04a7..fd0a4ebaa 100644 --- a/macros/tables/link_template.sql +++ b/macros/tables/link_template.sql @@ -13,14 +13,14 @@ limitations under the License. -#} {%- macro link_template(src_pk, src_fk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_fk, tgt_ldts, tgt_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, source) -%} - -{%- set is_union = true if source|length > 1 else false -%} -- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault +{% set is_union = true if source|length > 1 else false %} SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_ldts, tgt_source], 'stg') }} FROM ( - {{ dbtvault.create_source(src_pk, src_fk, src_ldts, src_source, tgt_cols, tgt_pk|last, + {{ dbtvault.create_source(src_pk, src_fk, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, source, is_union) }} ) AS stg {% if is_incremental() or is_union -%} diff --git a/macros/tables/sat_template.sql b/macros/tables/sat_template.sql index cd1a7ec19..c3493c261 100644 --- a/macros/tables/sat_template.sql +++ b/macros/tables/sat_template.sql @@ -14,11 +14,13 @@ -#} {%- macro sat_template(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source, source) -%} -- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault +{%- set tgt_cols = dbtvault.get_tgt_cols([tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source]) %} + SELECT DISTINCT {{ dbtvault.cast([tgt_hashdiff, tgt_pk, tgt_payload, tgt_ldts, tgt_eff, tgt_source], 'e') }} FROM {{ source[0] }} AS e {% if is_incremental() -%} From cf52d0fbe84149ced73f0666ee7e4375264725fc Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 7 Oct 2019 17:09:18 +0100 Subject: [PATCH 064/157] Updated latest stable version --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c640b8191..ee324ac60 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.1-pre)](https://dbtvault.readthedocs.io/en/v0.1-pre/?badge=v0.1-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.2-pre)](https://dbtvault.readthedocs.io/en/v0.2-pre/?badge=v0.2-pre) # dbtvault by [Datavault](https://www.data-vault.co.uk) @@ -27,7 +27,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.1-pre # Latest stable version + revision: v0.2-pre # Latest stable version ``` And run ```dbt deps``` From 4f7b68eaae29ae5a4f5877789dec9dcc3e840eb6 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 7 Oct 2019 17:14:47 +0100 Subject: [PATCH 065/157] Added line to source section --- docs/staging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/staging.md b/docs/staging.md index ba44f33a7..85f16fbe8 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -56,7 +56,7 @@ in our model. {%- set source_table = source('MYSOURCE', 'stg_customer') -%} ``` - +For more information on the ```source``` macro, please refer [here](gettingstarted.md#setting-up-sources) ### Adding the metadata From de3c6af652373fe5d833d6a0017d6df97837dbf7 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 7 Oct 2019 18:45:50 +0100 Subject: [PATCH 066/157] Minor Documentation additions - Minor additions and corrections to documentation - Fixed website URL in footer - Added contribution page to docs - Corrected version in dbt_project.yml --- CONTRIBUTING.md | 5 ++++- README.md | 4 ++-- dbt_project.yml | 2 +- docs/contributing.md | 36 ++++++++++++++++++++++++++++++++++++ docs/gettingstarted.md | 12 ++++++++++-- docs/staging.md | 6 +++++- mkdocs.yml | 3 ++- 7 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 docs/contributing.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff30417b0..cf3e470d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ Happy Data Vaulting! We've tested the package rigorously, but if you think you've found a bug please provide the following at a minimum (or use the issue templates) so we can fix it as quickly as possible: -- The version of dbtvault being used (as we're still in pre-release, this can be omitted) +- The version of dbtvault being used. - Steps to reproduce the issue - Any error messages or dbt log files which can give more detail of the problem @@ -28,6 +28,9 @@ at a minimum (or use the issue templates) so we can fix it as quickly as possibl We'd love to add new features to make this package even more useful for the community, please feel free to submit ideas and thoughts! +### If it's an idea, feedback or a general inquiry +Create a post with as much detail as possible; We'll be happy to reply and work with you. + ## Pull requests If you've developed something which we can add via a pull request, we'd prefer that you submit an issue first so that we can discuss the changes. \ No newline at end of file diff --git a/README.md b/README.md index ee324ac60..f29e0235d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.2-pre)](https://dbtvault.readthedocs.io/en/v0.2-pre/?badge=v0.2-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.2.1-pre)](https://dbtvault.readthedocs.io/en/v0.2.1-pre/?badge=v0.2.1-pre) # dbtvault by [Datavault](https://www.data-vault.co.uk) @@ -27,7 +27,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.2-pre # Latest stable version + revision: v0.2.1-pre # Latest stable version ``` And run ```dbt deps``` diff --git a/dbt_project.yml b/dbt_project.yml index 9c130b001..e026a821d 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,5 +1,5 @@ name: 'dbtvault' -version: '1.0' +version: 'v0.2.1-pre' profile: 'dbtvault' diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 000000000..4611e14d3 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,36 @@ +## We'd love to hear from you + +This dbtvault package is very much a work in progress – we’ll up the version number to 1.0 when we’re satisfied it +works out in the wild. + +We know that it deserves new features, that the code base can be tidied up and the SQL better tuned. +Rest assured we’re working on it for future releases – our roadmap contains information on what’s coming. + +If you spot anything you’d like to bring to our attention, have a request for new features, +have spotted an improvement we could make, or want to tell us about a typo, then please don’t hesitate to let us know +by submitting an issue using the below guidelines + +We’d rather know you are making active use of this package than hearing nothing from all of you out there! + +Happy Data Vaulting! :smile: + +## Issue guidelines + +### If it's a bug +We've tested the package rigorously, but if you think you've found a bug please provide the following +at a minimum (or use the issue templates) so we can fix it as quickly as possible: + +- The version of dbtvault being used. +- Steps to reproduce the issue +- Any error messages or dbt log files which can give more detail of the problem + +### If it's a feature request +We'd love to add new features to make this package even more useful for the community, +please feel free to submit ideas and thoughts! + +### If it's an idea, feedback or a general inquiry +Create a post with as much detail as possible; We'll be happy to reply and work with you. + +## Pull requests +If you've developed something which we can add via a pull request, we'd prefer that you submit an issue first +so that we can discuss the changes. \ No newline at end of file diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 7a5bcbae7..991ab5551 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -27,8 +27,6 @@ Happy Data Vaulting! :smile: 6. Our macros assume that you are only loading from one set of load dates in a single load cycle (i.e. Your staging layer contains data for one ```load_datetime``` value only). **We will be removing this restriction in future releases**. - - ## Setting up sources We will be using the ```source``` feature of dbt extensively throughout the documentation to make access to source @@ -71,3 +69,13 @@ And run [Read more on package installation (from dbt)](https://docs.getdbt.com/docs/package-management) + +## Final note before we start + +The documentation is written in the context of a simple example, showing a step by step progression towards +loading a Data Vault 2.0 Data Warehouse. We have documented everything you need to know, but as all use cases will vary, +you will need to adapt this to your own needs and requirements. + +If you need any more detail or require specific guidance, do not hesitate to +[submit an issue](https://github.com/Datavault-UK/dbtvault/issues). +We may be able to improve the package based on your feedback, and this will benefit the whole community! \ No newline at end of file diff --git a/docs/staging.md b/docs/staging.md index 85f16fbe8..4f84f094c 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -48,6 +48,10 @@ Usually we want hashing layers to be views. Next we will create a variable which holds a reference to the raw source table, since we will need to refer to it a few times in our model. +!!! note + If you have not yet set up sources in your dbt configuration please refer [here](gettingstarted.md#setting-up-sources). + + ```stg_orders_hashed.sql``` ```sql hl_lines="3" @@ -56,7 +60,7 @@ in our model. {%- set source_table = source('MYSOURCE', 'stg_customer') -%} ``` -For more information on the ```source``` macro, please refer [here](gettingstarted.md#setting-up-sources) + ### Adding the metadata diff --git a/mkdocs.yml b/mkdocs.yml index 3cb743c58..86a795d42 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,12 +28,13 @@ nav: - Demonstration: 'demonstration.md' - Roadmap: 'roadmap.md' - Changelog: 'changelog.md' + - Contributing: 'contributing.md' - Licence: 'LICENSE.md' extra: social: - type: 'globe' - link: 'www.data-vault.co.uk' + link: 'https://www.data-vault.co.uk' - type: 'github' link: 'https://github.com/Datavault-UK/' - type: 'twitter' From 44b62eb61fb9c166fba90f947a98fcdf72823c75 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 7 Oct 2019 19:06:23 +0100 Subject: [PATCH 067/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f29e0235d..401a6e8ba 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ And run {%- set source = ... -%} {{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_cols, tgt_pk, tgt_nk, tgt_ldts, tgt_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, source) }} ``` From 85a1b869cee6029d8117d9d900c03e7fdef88533 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 7 Oct 2019 19:08:37 +0100 Subject: [PATCH 068/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 401a6e8ba..ae73a5af2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**CURRENTLY IN PRE-RELEASE, STAY TUNED FOR AN OFFICAL ANNOUNCEMENT AND FULL DOCUMENTATION** +**CURRENTLY IN PRE-RELEASE, WE ARE CONTINUALLY ADDING FEATURES AND IMPROVING DOCUMENTATION**

From 60a481c01b61fef22e51cb8799976588e1d16246 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 7 Oct 2019 21:39:07 +0100 Subject: [PATCH 069/157] Fixed version in project file --- dbt_project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbt_project.yml b/dbt_project.yml index e026a821d..4482bbaec 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,5 +1,5 @@ name: 'dbtvault' -version: 'v0.2.1-pre' +version: '0.2.1' profile: 'dbtvault' From 5e8e18af5705f3ffdf47e8b143ad65f71c50e349 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 8 Oct 2019 13:38:08 +0100 Subject: [PATCH 070/157] Added satellite and union information to core docs - Updated staging with satellite fields (Hashdiff) --- docs/hubs.md | 56 ++++++++++-- docs/links.md | 54 +++++++++++- docs/satellites.md | 215 +++++++++++++++++++++++++++++++++++++++++++++ docs/staging.md | 61 +++++++------ 4 files changed, 350 insertions(+), 36 deletions(-) diff --git a/docs/hubs.md b/docs/hubs.md index ec7c47fe2..454cf3d7e 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -122,7 +122,7 @@ dbt ensures dependencies are honoured when defining the source using a reference {%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} {%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} -{%- set source = [ref('stg_orders_hashed')] -%} +{%- set source = [ref('stg_customer_hashed')] -%} ``` !!! note @@ -133,8 +133,7 @@ dbt ensures dependencies are honoured when defining the source using a reference Now we bring it all together and call the [hub_template](macros.md#hub_template) macro: -```hub_customer.sql``` - +```hub_customer.sql``` ```sql hl_lines="15 16 17" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} @@ -148,7 +147,7 @@ Now we bring it all together and call the [hub_template](macros.md#hub_template) {%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} {%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} -{%- set source = [ref('stg_orders_hashed')] -%} +{%- set source = [ref('stg_customer_hashed')] -%} {{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, tgt_pk, tgt_nk, tgt_ldts, tgt_source, @@ -163,7 +162,7 @@ With our model complete, we can run dbt to create our ```hub_customer``` hub. !!! tip Using the '+' in the command above will get dbt to compile and run all parent dependencies for the model we are - running, in this case, it will re-create the staging layer from the ```stg_orders_hashed``` model if needed. + running, in this case, it will re-create the staging layer from the ```stg_customer_hashed``` model if needed. dbt will also create our hub if it doesn't already exist. And our table will look like this: @@ -175,6 +174,53 @@ And our table will look like this: | . | . | . | . | | FED333... | 1004 | 1993-01-01 | 1 | +### Loading from multiple sources to form a union-based hub + +In some cases, we may need to create a hub via a union, instead of a single source as we have seen so far. +This may be because: + +- Another raw staging table holds some records which our single source does not, and the tables share +a key. +- We have multiple source-systems containing different versions or parts of the data which we need to combine. + +We know this data can and should be combined because these records have a shared key. +We can union the tables on that key, and create a hub containing a complete record set. + +We'll need to create a [staging model](staging.md) for each of the sources involved, +and provide them as a list of references to the source parameter as shown below. + +!!! note + If your primary key and natural key columns have different names across the different + tables, they will need to be aliased to the same name in the respective staging layers + via the [add_columns](macros.md#add_columns) macro. In future releases we will add + the ability to alias the columns at this stage in the hub model itself too. + + +This procedure requires additional metadata in our ```hub_customer``` model, +and the [hub_template](macros.md#hub_template) will handle the rest: + +```hub_customer.sql``` +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags=['hub', 'union']) -}} + +{%- set src_pk = ['CUSTOMER_PK', 'CUSTOMER_PK', 'CUSTOMER_PK'] -%} +{%- set src_nk = ['CUSTOMER_ID', 'CUSTOMER_ID', 'CUSTOMER_ID'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} +{%- set tgt_nk = [src_nk[0], 'NUMBER(38,0)', src_nk[0]] -%} +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_sap_customer_hashed'), + ref('stg_crm_customer_hashed'), + ref('stg_web_customer_hashed')] -%} + +{{ dbtvault.hub_template(src_pk, src_fk, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} +``` ### Next steps diff --git a/docs/links.md b/docs/links.md index df3b50512..f09374612 100644 --- a/docs/links.md +++ b/docs/links.md @@ -121,7 +121,7 @@ the staging layer model we made earlier, as this contains all the columns we nee {%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} {%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} -{%- set source = [ref('stg_orders_hashed')] -%} +{%- set source = [ref('stg_customer_hashed')] -%} ``` !!! note @@ -148,7 +148,7 @@ Now we bring it all together and call the [link_template](macros.md#link_templat {%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} {%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} -{%- set source = [ref('stg_orders_hashed')] -%} +{%- set source = [ref('stg_customer_hashed')] -%} {{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, tgt_pk, tgt_fk, tgt_ldts, tgt_source, @@ -171,6 +171,56 @@ And our table will look like this: | . | . | . | . | . | | 1CE6A9... | FED333... | D78382... | 1993-01-01 | 1 | +### Loading from multiple sources to form a union-based link + +In some cases, we may need to create a link via a union, instead of a single source as we have seen so far. +This may be because: + +- Another raw staging table holds some records which our single source does not, and the tables share +a key. +- We have multiple source-systems containing different versions or parts of the data which we need to combine. + +We know this data can and should be combined because these records have a shared key. +We can union the tables on that key, and create a hub containing a complete record set. + +We'll need to create a [staging model](staging.md) for each of the sources involved, +and provide them as a list of references to the source parameter as shown below. + +!!! note + If your primary key and natural key columns have different names across the different + tables, they will need to be aliased to the same name in the respective staging layers + via the [add_columns](macros.md#add_columns) macro. In future releases we will add + the ability to alias the columns at this stage in the hub model itself too. + + +This procedure requires additional metadata in our ```link_customer_nation``` model, +and the [link_template](macros.md#link_template) will handle the rest: + +```link_customer_nation.sql``` +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags=['link', 'union']) -}} + +{%- set src_pk = ['CUSTOMER_NATION_PK', 'CUSTOMER_NATION_PK', 'CUSTOMER_NATION_PK'] -%} +{%- set src_fk = [['CUSTOMER_PK', 'NATION_PK'], ['CUSTOMER_PK', 'NATION_PK'], + ['CUSTOMER_PK', 'NATION_PK']] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} +{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} + +{%- set source = [ref('stg_sap_customer_hashed'), + ref('stg_crm_customer_hashed'), + ref('stg_web_customer_hashed')] -%} + +{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} +``` ### Next steps diff --git a/docs/satellites.md b/docs/satellites.md index e69de29bb..fdefaba9f 100644 --- a/docs/satellites.md +++ b/docs/satellites.md @@ -0,0 +1,215 @@ +Satellites compliment hubs and links, providing more concrete data and temporal attributes. + +They will usually consist of the following columns: + +1. A primary key (or surrogate key) which is usually a hashed representation of the natural key. + +2. A hashdiff. This is a concatenation of the payload (below) and the primary key. This +allows us to detect changes in a record. For example, if a customer changes their name, +the hashdiff will change as a result of the payload changing. + +3. A payload. The payload consists of concrete data for an entity, i.e. a customer record. This could be +a name, a date of birth, nationality, age, gender or more. The payload will contain some or all of the +concrete data for an entity, depending on the purpose of the satellite. + +4. An effectivity date. Usually called ```EFFECTIVE_FROM```, this column is the key temporal attribute of a +satellite record. The main purpose of this column is to record that a record is valid at a specific point in time. +If a customer changes their name, then the record with their 'old' name should no longer be valid, and it will no longer +have the most recent ```EFFECTIVE_FROM```. + +5. The load date or load date timestamp. This identifies when the record was first loaded into the vault. + +6. The source for the record. + +### Creating the model header + +Create a new dbt model as before. We'll call this one ```sat_customer_details```. + +The following header is what we use, but feel free to customise it to your needs: + +```sat_customer_details.sql``` +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} +``` + +Satellites are always incremental, as we load and add new records to the existing data set. + +An incremental materialisation will optimize our load in cases where the target table (in this case, ```sat_customer_details```) +already exists and already contains data. This is very important for tables containing a lot of data, where every ounce +of optimisation counts. + +[Read more about incremental models](https://docs.getdbt.com/docs/configuring-incremental-models) + +### Adding the metadata + +Let's look at the metadata we need to provide to the [sat_template](macros.md#sat_template) macro. + +#### Source columns + +Using our knowledge of what columns we need in our ```sat_customer_details``` table, we can identify columns in our +staging layer which map to them: + +1. A primary key, which is a hashed natural key. The ```CUSTOMER_PK``` we created earlier in the [staging](staging.md) section +is a perfect fit. +2. A hashdiff. We created ```CUSTOMER_HASHDIFF``` in [staging](staging.md) earlier, which we will use here. +3. Some payload columns: ```CUSTOMER_NAME```, ```CUSTOMER_DOB```, ```CUSTOMER_PHONE``` which should be present in the +raw staging layer via an [add_columns](macros.md#add_columns) macro call. +4. An ```EFFECTIVE_FROM``` column, also added in staging. +5. A load date timestamp, which is present in the staging layer as ```LOADDATE```. +6. A ```SOURCE``` column. + +We can now add this metadata to the model: + +```sat_customer_details.sql``` +```sql hl_lines="3 4 5 7 8 9" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} +{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} + +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} +``` + +#### Target columns + +Now we can define the target column mapping. The [sat_template](macros.md#sat_template) does a lot of work for us if we +provide the metadata it requires. We can define which source columns map to the required target columns and +define a column type at the same time: + +```sat_customer_details.sql``` +```sql hl_lines="11 12 13 14 15 17 18 19" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} +{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} + +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} +{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} +{%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], + [ src_payload[1], 'DATE', 'DOB'], + [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} + +{%- set tgt_eff = ['EFFECTIVE_FROM', 'DATE', 'EFFECTIVE_FROM'] -%} +{%- set tgt_ldts = ['LOADDATE', 'DATE', 'LOADDATE'] -%} +{%- set tgt_source = ['SOURCE', 'VARCHAR(15)', 'SOURCE'] -%} + +``` + +With these 6 additional lines, we have now informed the macro how to transform our source data: + +- We have provided our mapping from source to target. We're renaming the payload columns and the hashdiff here. +We are removing the ```CUSTOMER``` prefix, as this satellite is specifically for customer details and it's +superfluous. Renaming will always depend on your specific project and context, however. + +- We have provided a type in the mapping so that the type is explicitly defined. For now, this is not optional, but +in future releases we will simplify this for scenarios where we want the data type or column name to remain unchanged. + +!!! info + There is nothing to stop you entering invalid type mappings in this step (i.e. trying to cast an invalid date format to a date), + so please ensure they are correct. + You will soon find out, however, as dbt will issue a warning to you. No harm done, but save time by providing + accurate metadata! + +#### Source table + +The last piece of metadata we need is the source table. This step is easy, as in this example we created the +new staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. +dbt ensures dependencies are honoured when defining the source using a reference in this way. + +[Read more about the ref function](https://docs.getdbt.com/docs/ref) + +```sat_customer_details.sql``` +```sql hl_lines="21" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} +{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} + +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} +{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} +{%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], + [ src_payload[1], 'DATE', 'DOB'], + [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} + +{%- set tgt_eff = ['EFFECTIVE_FROM', 'DATE', 'EFFECTIVE_FROM'] -%} +{%- set tgt_ldts = ['LOADDATE', 'DATE', 'LOADDATE'] -%} +{%- set tgt_source = ['SOURCE', 'VARCHAR(15)', 'SOURCE'] -%} + +{%- set source = [ref('stg_customer_hashed')] -%} + +``` + +!!! note + Make sure you surround the ref call with square brackets, as shown in the snippet + above. + +### Invoking the template + +Now we bring it all together and call the [sat_template](macros.md#sat_template) macro: + +```sat_customer_details.sql``` +```sql hl_lines="23 24 25 26 27" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} +{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} + +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} +{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} +{%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], + [ src_payload[1], 'DATE', 'DOB'], + [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} + +{%- set tgt_eff = ['EFFECTIVE_FROM', 'DATE', 'EFFECTIVE_FROM'] -%} +{%- set tgt_ldts = ['LOADDATE', 'DATE', 'LOADDATE'] -%} +{%- set tgt_source = ['SOURCE', 'VARCHAR(15)', 'SOURCE'] -%} + +{%- set source = [ref('stg_customer_hashed')] -%} + +{{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source, + source) }} + +``` + +### Running dbt + +With our model complete, we can run dbt to create our ```sat_customer_details``` satellite. + +```dbt run --models +sat_customer_details``` + +And our table will look like this: + +| CUSTOMER_PK | HASHDIFF | NAME | DOB | PHONE | EFFECTIVE_FROM | LOADDATE | SOURCE | +| ------------ | ------------ | ---------- | ------------ | --------------- | -------------- | ----------- | ------ | +| B8C37E... | 3C5984... | Alice | 1997-04-24 | 17-214-233-1214 | 1993-01-01 | 1993-01-01 | 1 | +| . | . | . | . | . | . | . | 1 | +| . | . | . | . | . | . | . | 1 | +| FED333... | D8CB1F... | Dom | 2018-04-13 | 17-214-233-1217 | 1993-01-01 | 1993-01-01 | 1 | + + +### Next steps + +We have now created a staging layer and a hub, link and satellite. We'll be bringing new +table structures in future releases. We'll also be releasing material which demonstrates these examples in a live +environment soon! \ No newline at end of file diff --git a/docs/staging.md b/docs/staging.md index 4f84f094c..68c2f8680 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -20,16 +20,12 @@ Specifically, we need to add primary key hashes, hashdiffs, and any implied fixe ### Creating the model header First we create a new dbt model. Our source table is called ```stg_customer``` -and we should name our additional layer ```stg_orders_hashed```, although any sensible naming convention will work if -kept consistent. In this case, we create a new file ```stg_orders_hashed.sql``` in our models folder. - -!!! info - We are using the name ```stg_orders_hashed``` for reasons that will become clear as we progress through the guide. - Our hubs, links and satellites will require more than just customer data, and so ```orders``` makes more sense. +so we should name our additional layer ```stg_customer_hashed```, although any sensible naming convention will work if +kept consistent. In this case, we create a new file ```stg_customer_hashed.sql``` in our models folder. Let's start by adding the model header to the file: -```stg_orders_hashed.sql``` +```stg_customer_hashed.sql``` ```sql {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} @@ -52,7 +48,7 @@ in our model. If you have not yet set up sources in your dbt configuration please refer [here](gettingstarted.md#setting-up-sources). -```stg_orders_hashed.sql``` +```stg_customer_hashed.sql``` ```sql hl_lines="3" {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} @@ -74,20 +70,22 @@ provided in the link above. After adding the macro call, our model will now look something like this: -```stg_orders_hashed.sql``` -```sql hl_lines="5 6 7" +```stg_customer_hashed.sql``` +```sql hl_lines="5 6 7 8 9" -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} -{%- set source_table = source('MYSOURCE', 'stg_customer') -%} +{%- set source_table = source('MYSOURCE', 'stg_customer') -%} {{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), ('NATION_ID', 'NATION_PK'), - (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK')]) -}}, + (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), + (['CUSTOMER_ID', 'CUSTOMER_NAME', + 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], 'CUSTOMER_HASHDIFF')]) -}}, ``` !!! note - Make sure you add the trailing comma after the call, at the end of line 7. + Make sure you add the trailing comma after the call, at the end of line 9. This call will: @@ -97,8 +95,11 @@ value. value. - Concatenate the values in the ```CUSTOMER_ID``` and ```NATION_ID``` columns and hash them, creating a new column called ```CUSTOMER_NATION_PK``` containing the hash of the combination of the values. +- Concatenate the values in the ```CUSTOMER_ID```, ```CUSTOMER_NAME```, ```CUSTOMER_PHONE```, ```CUSTOMER_DOB``` +columns and hash them, creating a new column called ```CUSTOMER_NATION_PK``` containing the hash of the +combination of the values. -The latter two pairs will be used later when creating [links](links.md). +The latter three pairs will be used later when creating [links](links.md) and [satellites](satellites.md). ### Additional columns @@ -121,20 +122,22 @@ later when creating the raw vault tables. We can also use this method to create exist in the source. -```stg_orders_hashed.sql``` -```sql hl_lines="9 10 11" +```stg_customer_hashed.sql``` +```sql hl_lines="11 12 13" -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} -{%- set source_table = source('MYSOURCE', 'stg_customer') -%} +{%- set source_table = source('MYSOURCE', 'stg_customer') -%} {{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), ('NATION_ID', 'NATION_PK'), - (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK')]) -}}, + (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), + (['CUSTOMER_ID', 'CUSTOMER_NAME', + 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], 'CUSTOMER_HASHDIFF')]) -}}, {{ dbtvault.add_columns(source_table, [('!1', 'SOURCE'), - ('LOADDATE', 'EFFECTIVE_FROM')]) }} + ('LOADDATE', 'EFFECTIVE_FROM')]) }} ``` @@ -166,7 +169,7 @@ macro. After adding the footer, our completed model should now look like this: -```stg_orders_hashed.sql``` +```stg_customer_hashed.sql``` ```sql hl_lines="13" {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} @@ -191,16 +194,16 @@ This model is now ready to run to create a view with all the added data/columns With our model complete, we can run dbt and have our new staging layer materialised as configured in the header: -```dbt run --models stg_orders_hashed``` +```dbt run --models stg_customer_hashed``` And our table will look like this: -| CUSTOMER_PK | NATION_PK | CUSTOMER_NATION_PK | (source table columns) | EFFECTIVE_FROM | SOURCE | -| ------------ | ------------ | ------------------- | ---------------------- | -------------- | ------------ | -| B8C37E... | D89F3A... | 72A160... | . | 1993-01-01 | 1 | -| . | . | . | . | . | . | -| . | . | . | . | . | . | -| FED333... | D78382... | 1CE6A9... | . | 1993-01-01 | 1 | +| CUSTOMER_PK | NATION_PK | CUSTOMER_NATION_PK | CUSTOMER_HASHDIFF | (source table columns) | EFFECTIVE_FROM | SOURCE | +| ------------ | ------------ | ------------------- | ------------------- | ---------------------- | -------------- | ------------ | +| B8C37E... | D89F3A... | 72A160... | . | . | 1993-01-01 | 1 | +| . | . | . | . | . | . | . | +| . | . | . | . | . | . | . | +| FED333... | D78382... | 1CE6A9... | . | . | 1993-01-01 | 1 | ### Next steps From da4f6995bae8dea5251daa73fe594c69adcea898 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 8 Oct 2019 13:46:21 +0100 Subject: [PATCH 071/157] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ae73a5af2..43e2a4ec2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.2.1-pre)](https://dbtvault.readthedocs.io/en/v0.2.1-pre/?badge=v0.2.1-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.2.2-pre)](https://dbtvault.readthedocs.io/en/v0.2.2-pre/?badge=v0.2.2-pre) # dbtvault by [Datavault](https://www.data-vault.co.uk) @@ -27,7 +27,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.2.1-pre # Latest stable version + revision: v0.2.2-pre # Latest stable version ``` And run ```dbt deps``` From d88518a75966eacbc9f4327b84c7756e49e6cd4f Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 8 Oct 2019 13:50:23 +0100 Subject: [PATCH 072/157] Updated changelog --- docs/changelog.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 8fc115dc9..775b45b56 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.2.2-pre] - 2019-10-08 + +### Documentation + +- Finished Satellite page +- Added Union sections to Hub and Link pages +- Updated staging page with Satellite fields +- Renamed ```stg_orders_hashed``` back to ```stg_customers_hashed``` + +## [v0.2.1-pre] - 2019-10-07 + +### Documentation + +- Minor additions and corrections to documentation: + - Fixed website URL in footer + - Added contribution page to docs + - Corrected version in dbt_project.yml ## [v0.2-pre] - 2019-10-07 From c37c71bc3fb034c10b20c6e8df882caca4ed6325 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 8 Oct 2019 17:07:21 +0100 Subject: [PATCH 073/157] Updated hashing macros - Hashing macros now require a boolean flag to alpha-sort columns. This flag should be set to true on hashdiffs. --- docs/macros.md | 31 ++++++++++++++++++------------- docs/staging.md | 23 ++++++++++++----------- macros/staging/multi_hash.sql | 18 +++++++++++++----- macros/supporting/hash.sql | 4 ++-- 4 files changed, 45 insertions(+), 31 deletions(-) diff --git a/docs/macros.md b/docs/macros.md index c8318844b..bb75bf0e3 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -388,18 +388,20 @@ CAST(MD5_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(16)) AS alias2 #### Parameters -| Parameter | Description | Type | Required? | -| ---------------- | ---------------------------------------------- | ------ | -------------------------------------------------------- | -| pairs | (column, alias) pair | Tuple | check_circle | -| pairs: columns | Single column string or list of columns | String | check_circle | -| pairs: alias | The alias for the column | String | check_circle | +| Parameter | Description | Type | Required? | +| ---------------- | ---------------------------------------------- | -------- | -------------------------------------------------------- | +| pairs | (column, alias) pair | Tuple | check_circle | +| pairs: columns | Single column string or list of columns | String | check_circle | +| pairs: alias | The alias for the column | String | check_circle | +| pairs: sort | Will alpha sort columns if true, default false. | Boolean | clear | #### Usage ```yaml {{ dbtvault.multi_hash([('CUSTOMERKEY', 'CUSTOMER_PK'), - (['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF')]) }} + (['CUSTOMERKEY', 'NAME', 'PHONE', 'DOB'], + 'HASHDIFF', true)]) }} ``` #### Output @@ -415,7 +417,8 @@ CAST(MD5_BINARY(CONCAT( ``` !!! success "Column sorting" - You do not need to worry about providing the columns in any particular order; Provided columns are alpha-sorted automatically, as per best practises. + You do not need to worry about providing the columns in any particular order as long as you set the + ```sort``` flag to true when creating hashdiffs. ___ @@ -582,23 +585,25 @@ CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias ``` - Can provide multiple columns as a list to create a concatenated hash -- When multiple columns are provided, they are alpha-sorted automatically. +- Hashdiffs should be alpha sorted using the ```sort``` flag. - Casts a column as ```VARCHAR```, transforms to ```UPPER``` case and trims whitespace - ```'^^'``` Accounts for null values with a double caret - ```'||'``` Concatenates with a double pipe #### Parameters -| Parameter | Description | Type | Required? | -| ---------------- | ---------------------------------------------- | ----------- | -------------------------------------------------------- | -| columns | Columns to hash on | String/List | check_circle | -| alias | The name to give the hashed column | String | check_circle | +| Parameter | Description | Type | Required? | +| ---------------- | ----------------------------------------------- | ----------- | -------------------------------------------------------- | +| columns | Columns to hash on | String/List | check_circle | +| alias | The name to give the hashed column | String | check_circle | +| sort | Will alpha sort columns if true, default false. | Boolean | clear | + #### Usage ```yaml {{ dbtvault.hash('CUSTOMERKEY', 'CUSTOMER_PK') }}, -{{ dbtvault.hash(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'HASHDIFF') }} +{{ dbtvault.hash(['CUSTOMERKEY', 'PHONE', 'DOB', 'NAME'], 'HASHDIFF', true) }} ``` !!! tip diff --git a/docs/staging.md b/docs/staging.md index 68c2f8680..e5c07ac1f 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -71,17 +71,18 @@ provided in the link above. After adding the macro call, our model will now look something like this: ```stg_customer_hashed.sql``` -```sql hl_lines="5 6 7 8 9" +```sql hl_lines="5 6 7 8 9 10" -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} - -{%- set source_table = source('MYSOURCE', 'stg_customer') -%} - -{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), - ('NATION_ID', 'NATION_PK'), - (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), - (['CUSTOMER_ID', 'CUSTOMER_NAME', - 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], 'CUSTOMER_HASHDIFF')]) -}}, +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} + +{%- set source_table = source('MYSOURCE', 'stg_customer') -%} + +{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), + ('NATION_ID', 'NATION_PK'), + (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), + (['CUSTOMER_ID', 'CUSTOMER_NAME', + 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], + 'CUSTOMER_HASHDIFF', true)]) -}}, ``` !!! note @@ -97,7 +98,7 @@ value. column called ```CUSTOMER_NATION_PK``` containing the hash of the combination of the values. - Concatenate the values in the ```CUSTOMER_ID```, ```CUSTOMER_NAME```, ```CUSTOMER_PHONE```, ```CUSTOMER_DOB``` columns and hash them, creating a new column called ```CUSTOMER_NATION_PK``` containing the hash of the -combination of the values. +combination of the values. The ```true``` parameter should be provided so that the columns are alpha-sorted. The latter three pairs will be used later when creating [links](links.md) and [satellites](satellites.md). diff --git a/macros/staging/multi_hash.sql b/macros/staging/multi_hash.sql index a76410f9c..81fd8a5a1 100644 --- a/macros/staging/multi_hash.sql +++ b/macros/staging/multi_hash.sql @@ -12,12 +12,20 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro multi_hash(pairs) -%} +{%- macro multi_hash(triples) -%} -- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault SELECT -{% for pair in pairs -%} - {{ dbtvault.hash(pair[0], pair[1]) }} - {%- if not loop.last -%} , - {% endif %} +{% for triple in triples -%} + {%- if triple | length == 2 -%} + + {{ dbtvault.hash(triple[0], triple[1]) }} + + {%- elif triple | length == 3 and triple | last == true -%} + + {{ dbtvault.hash(triple[0], triple[1], triple[2]) }} + + {%- endif -%} + + {% if not loop.last -%}, {% endif %} {%- endfor -%} {%- endmacro -%} diff --git a/macros/supporting/hash.sql b/macros/supporting/hash.sql index 707f57e06..e78d0f353 100644 --- a/macros/supporting/hash.sql +++ b/macros/supporting/hash.sql @@ -12,10 +12,10 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro hash(columns, alias) -%} +{%- macro hash(columns, alias, sort=false) -%} {#- Alpha sort columns before hashing -#} -{%- if columns is iterable and columns is not string -%} +{%- if sort and columns is iterable and columns is not string -%} {%- set columns = columns|sort -%} {%- endif -%} From fe859f84c01fec8728c7b95e72adeb8e0d6c3560 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 8 Oct 2019 17:11:34 +0100 Subject: [PATCH 074/157] Updated Changelog for 0.2.3 --- docs/changelog.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 775b45b56..1d4dd9fdc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.2.3-pre] - 2019-10-08 + +### Macros + +- Updated [hash](macros.md#hash) and [multi-hash](macros.md#multi_hash) + - [hash](macros.md#hash) now accepts a third parameter, ```sort``` + which will alpha-sort provided columns when set to true. + - [multi-hash](macros.md#multi_hash) updated to take advantage of + the the [hash](macros.md#hash) functionality. + +### Documentation + +- Updated [hash](macros.md#hash) and [multi-hash](macros.md#multi_hash) according to new changes. + ## [v0.2.2-pre] - 2019-10-08 ### Documentation From 801896a93d017ab6f5d429df40ff463bc904c9cd Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 8 Oct 2019 17:16:19 +0100 Subject: [PATCH 075/157] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 43e2a4ec2..46786ac4d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.2.2-pre)](https://dbtvault.readthedocs.io/en/v0.2.2-pre/?badge=v0.2.2-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.2.3-pre)](https://dbtvault.readthedocs.io/en/v0.2.3-pre/?badge=v0.2.3-pre) # dbtvault by [Datavault](https://www.data-vault.co.uk) @@ -27,7 +27,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.2.2-pre # Latest stable version + revision: v0.2.3-pre # Latest stable version ``` And run ```dbt deps``` From ff91f6cd8804445615f68f5277684e0c53957064 Mon Sep 17 00:00:00 2001 From: DVNeil Date: Fri, 11 Oct 2019 10:16:35 +0100 Subject: [PATCH 076/157] Updated readme to list benefits --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 46786ac4d..28f1a534e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,14 @@ stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/ # dbtvault by [Datavault](https://www.data-vault.co.uk) -dbtvault is a dbt package that generates & executes the ETL you need to run a Data Vault 2.0 Data Warehouse on a Snowflake database; +Build your own Data Vault data warehouse! dbtvault is a free to use dbt package that generates & executes the ETL you need to run a Data Vault 2.0 Data Warehouse on a Snowflake database. + +What does dbtvault offer? +- productivity gains, fewer errors +- multi-threaded execution of the generated SQL +- your data modeller can generate most of the ETL code directly from their mapping metadata +- your ETL developers can focus on the 5% of the SQL code that is different +- dbt generates documentation and data flow diagrams powered by [dbt](https://www.getdbt.com/), a registered trademark of [Fishtown Analytics](https://www.fishtownanalytics.com/) From d035082f6c21af6c7bca9f2cf025cd91924586f4 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 17 Oct 2019 00:23:05 +0100 Subject: [PATCH 077/157] v0.2.4-pre Release - Fixed a bug where the target alias would be used instead of the source alias when incrementally loading a hub or link, causing subsequent loads after the initial load, to fail. --- README.md | 4 ++-- docs/changelog.md | 8 ++++++++ macros/tables/hub_template.sql | 2 +- macros/tables/link_template.sql | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 28f1a534e..b1eb0c5a7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.2.3-pre)](https://dbtvault.readthedocs.io/en/v0.2.3-pre/?badge=v0.2.3-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.2.4-pre)](https://dbtvault.readthedocs.io/en/v0.2.4-pre/?badge=v0.2.4-pre) # dbtvault by [Datavault](https://www.data-vault.co.uk) @@ -34,7 +34,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.2.3-pre # Latest stable version + revision: v0.2.4-pre # Latest stable version ``` And run ```dbt deps``` diff --git a/docs/changelog.md b/docs/changelog.md index 1d4dd9fdc..fa1bfff2d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.2.4-pre] - 2019-10-17 + +### Bug Fixes + +- Fixed a bug where the target alias would be used instead of the source alias when incrementally loading a hub or link, +causing subsequent loads after the initial load, to fail. + + ## [v0.2.3-pre] - 2019-10-08 ### Macros diff --git a/macros/tables/hub_template.sql b/macros/tables/hub_template.sql index ca7fb7b48..1ec136f8f 100644 --- a/macros/tables/hub_template.sql +++ b/macros/tables/hub_template.sql @@ -25,7 +25,7 @@ FROM ( ) AS stg {% if is_incremental() or is_union -%} LEFT JOIN {{ this }} AS tgt -ON {{ dbtvault.prefix([tgt_pk|last], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} +ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL {%- if is_union %} AND stg.FIRST_SOURCE IS NULL diff --git a/macros/tables/link_template.sql b/macros/tables/link_template.sql index fd0a4ebaa..33f81b955 100644 --- a/macros/tables/link_template.sql +++ b/macros/tables/link_template.sql @@ -25,7 +25,7 @@ FROM ( ) AS stg {% if is_incremental() or is_union -%} LEFT JOIN {{ this }} AS tgt -ON {{ dbtvault.prefix([tgt_pk|last], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} +ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL {%- if is_union %} AND stg.FIRST_SOURCE IS NULL From bf9907571e3212b2608cb43f380f779e56275ea3 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 24 Oct 2019 17:18:18 +0100 Subject: [PATCH 078/157] Upcoming 0.3 additions --- CONTRIBUTING.md | 4 +- README.md | 8 +- dbt_project.yml | 2 +- docs/assets/images/staging.png | Bin 14322 -> 135292 bytes docs/bestpractices.md | 46 ++++ docs/changelog.md | 28 ++- docs/contributing.md | 4 +- docs/demonstration.md | 2 +- docs/gettingstarted.md | 32 +-- docs/hubs.md | 175 ++++++------- docs/index.md | 34 ++- docs/links.md | 173 ++++++------- docs/macros.md | 372 +++++++++++++++------------- docs/roadmap.md | 53 ++-- docs/satellites.md | 125 +++++----- docs/staging.md | 94 +++---- macros/internal/check_relation.sql | 23 ++ macros/internal/create_tgt_cols.sql | 100 ++++++++ macros/internal/get_col_list.sql | 48 ++++ macros/internal/is_union.sql | 50 ++++ macros/internal/union.sql | 4 +- macros/tables/hub_template.sql | 12 +- macros/tables/link_template.sql | 12 +- macros/tables/sat_template.sql | 29 ++- mkdocs.yml | 1 + 25 files changed, 862 insertions(+), 569 deletions(-) create mode 100644 docs/bestpractices.md create mode 100644 macros/internal/check_relation.sql create mode 100644 macros/internal/create_tgt_cols.sql create mode 100644 macros/internal/get_col_list.sql create mode 100644 macros/internal/is_union.sql diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf3e470d4..002b24959 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,5 +32,5 @@ please feel free to submit ideas and thoughts! Create a post with as much detail as possible; We'll be happy to reply and work with you. ## Pull requests -If you've developed something which we can add via a pull request, we'd prefer that you submit an issue first -so that we can discuss the changes. \ No newline at end of file +If you've developed something which we can add via a pull request, we're more than happy to consider it, but we'd +like to discuss the changes first. \ No newline at end of file diff --git a/README.md b/README.md index b1eb0c5a7..fba39d9d9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -**CURRENTLY IN PRE-RELEASE, WE ARE CONTINUALLY ADDING FEATURES AND IMPROVING DOCUMENTATION** -

latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.2.4-pre)](https://dbtvault.readthedocs.io/en/v0.2.4-pre/?badge=v0.2.4-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3-pre)](https://dbtvault.readthedocs.io/en/v0.3-pre/?badge=v0.3-pre) + +[past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) # dbtvault by [Datavault](https://www.data-vault.co.uk) @@ -34,7 +34,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.2.4-pre # Latest stable version + revision: v0.3-pre # Latest stable version ``` And run ```dbt deps``` diff --git a/dbt_project.yml b/dbt_project.yml index 4482bbaec..70b74be93 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,5 +1,5 @@ name: 'dbtvault' -version: '0.2.1' +version: '0.3' profile: 'dbtvault' diff --git a/docs/assets/images/staging.png b/docs/assets/images/staging.png index 007a1ac670e5a0c8adf63f528ad640fef017f3c1..517975d72e29d9fc1a9d04bc88e71e660cbc39c2 100755 GIT binary patch literal 135292 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 Bg46&2 literal 14322 zcmd6Ogu-= z@*Usz_xm5-eLmZso##2RbDwiR*L~gB6ZT$1k&Kv*7ytl}DJ#9z1^{p&*yBsW``FKu z7pc#%UqmiSh9CfdgyQavGqD7@0sxo+%5UX9c;g^59USw4zB8yfU~&>)q<;PDN0tm9 zbd_|ao*qU~M;(U>Tnqk)v7J-+ttD4YC{@F0{+@*h5d+v{HqtWcegy2!)qX)i)jlR( zS|ZNR(qc#U zfA&?Y%ogJEP+pvz#bgWgy9F0J&=e-_uHqyKd>IqEGyC7 z23<29DRP9k(8=3=d+6H=SJau_lo&xbv7QBqv4mKiM%ih9m{01{Cd_Tw61WGbeKS{~ zh8gPJtcdG+)*%~N!O}F1UOT2leD(+j_W55vDDu_-^hv}|qaTZK`XJ)1Dq<;XUlh2?Z4u;!(gqR~71V_q>aat4he!)8h7z z*Ztdmh2|C4Y@{6CXP4s|x6NTp6VT@4H~QA{vw2e#h7sK$Ff zDq)qty&KSpW5G&a*_oy^##OrTur1gjl--#&|%3*ec>H`HvA=aaLERe7zB2slAK}IVXw!=G_bwtFU zgWn0J3kIK^v4=QHEyT9r{~Y`H!_6l!6eak z*9WI9XUI1Tuts$EnD{~mE8bm0!<~2yeS*bQMJZ;0+QIubj`|0@%#HYm;ttyy@&^kp z5OB3OT7ejn2^#9{@phPgFIn)@z?K(C{weQa);2;+xcxy2UsAcuZu~aSb<3;O-N3mq z-R3O84FzKB$g0$ib>El-Vfe{ON7ykD}qFj(5EbW@bE z=9fuKy=_OKUN68zM;Ko$^HIl-@Ze)Qhwvjth(;z>aM^pFtO)NPLQe>VonwvmQm;gq zRy=JsmSN|M?Fc{FUrRg-YVxbtb&dJOzN5wCmJqiF|6`fWr~o$GZgaE^s+JBu8(qEy z;Y!5T>9Wa(N9A2)Trrkn&B0xw9vy>@%Tbs~+cU%7=!` z`H{kn_W%IuZ};r{Bq#RY!^NcPGkYpTx~21`se?l_soS35b&>-B?+Vx0PamuKrm_-$ zBr~18pc1HFeb!a%W7fz)h;@X5+)4ZSp+y3dvzJ_IhL`V*Qb@n7U`L#PWU>&S|KifX zH_@(WHVti=qvz~6wG$-;e&lfaP6%>x>$b>oBelId_~L(?p*~d}f$mxT!n76|*dY=f zTlJh167u?Rqw`)jw{02OPc&h-bcfo{DSCYU=NH}qRdb0&{1C}YzhPBbZM)s(3zI^! z(kc4KcA~4$-uW4SQvK6cMfBm9Cs#E~KF0gf_{nMPW||!PlvO+^KKv*qY->ZL#i_72= zveQPC;6OyWPkux^j5H%_g6@!(?L2StQ z7p;)~^8ainD3>>yG7j@NJGp4bot*-SS3z}t=5YS`oV6Pxsd zXwxZ(r6?|Z=i$Ygq$9IW4>9@~MTa&O%ihR*$T2CMEVhl5C)hbl67l_ZG#)_*6*V0V z$Nb$kC#klzlM+6yv_p7D7IJc`DEZ}rjVhB|de6Mh_ioSL$pjSd*X=(G2a(-1lJX>c z+GHxk^aF8}c&dPEdR;DJ=rfN!@Iiic4jev(%2sh}%Ou6L@x-t2QT-lInEuHekJyg^ ziZuYW<_n>DDhvG{cy?LA&y-=mhA+j0T@=}SGJVGQmuT>Z_Rzyt`>pQhtQ~o~5ZUP# z6&Q-0C>?lfRts-yL2Kxp{%HMFQ|1}#_YjK(ERv`)SFDRf(=+%s)2b$_x$7)G22?dY zKQ>eDtq!|LY{?D>4Z#r3M1+5hVx&MDzo3KH(@$!FsyIZNkFT__YhEDEipo8bC+JPn z-u6o4poo*sTREZLjT?Hdf3R4 z%lYT|AyrVp%sv;6tBo1A!Wvm>mT@>K|Bx{;6CQSU`*3^1o>X!0r3eh_x{aq5F-m(D z8BYFAWNn>0bD*eKN&8JOMGLhOYh`AyI~rt79Hciz=%$gUdg{mT(!cH8>5;^lbdW{T z5<-M6m8AMb@7zEP{_^n*GgH%`Wa*2hRA^qj3{zh@qn6tO2seG z+0+-zS4OZ~s<_zPE~oT`N~iVrrIedY=()`OjyJFGreP=+J(jw9`x6fa>pyMxoNr6M z7(VI(Dv{{BEXdHM=J5^G#oKad?t}dGb)X!>g-eJaN_U(d;$W<6u#QzfhJk**8WQ7Q zH&}qz`uk898kULqYNSu^>KqLU#_!~WlaI!ssQ)&xXA8Ep`>{PpLRj=qd}oz`T<^Q) zzR%k@YfVrdq*yz=Gl(U`$&R__oo~fa_l_iA>dcPe2cW9~u6goVevbVkeb)BScS0rv zxZcw!3@`G=j6>es=lemElH;=C?vv~pa3~N>c(CD zI3NLCwtfEeqZpN$1IG@DRf&P4(S<~2nB;3)Vc@^mt4HJww?+PV>RbMWfVkyAI;cxJ zW$GM(of`|zoqw!EdR*vG*8Iybdlw9pIE=F$vPurODRFr?+|l4`)s2}=?cp*T zyrlRO`E{u6F$XP_6|d;=Q%|;sM(v$C_}?5=-`LK~=vFqKDjXkz!pd9hemUBi%&?hw zB`~*?iAcmw(Rv``AkId3usE<4zo+z*)f(P6K2ls?a3gv{_q{WayUSNw=9o>yNvAX9 z;2ui|dB80UGD+@_Lfq^N3~!Mc>{r^@yYq_}r+tT>-nt6zAA0^-Rj&ZHi123G>D9VO zEPl<>m*ed2C8T+Q(%{x#EI~S(te&?;uTmJM2exsuj83d){LE~N)e~VynZ+@!6x~pV zZu8*};ql>RsdAb>WBMm*du8E--kuIXqerj*jNUeo%iMM%nj0I@)~!|Mw}l9F`?V$$ zBml{w7my%E79;D=Rbl6bU~mGf1V;@$b0{!*=3w5lKWpc~XYiTLBv~UuS9E~l!Tuyt0XRw(a0v}#6!x;WRjuvFmR|HEDUNixcZQqIm4$i+iqkEhiJseQ& z7yH8I=u^Z+BGc9JEjs#W0|)RaLbbcj4nyK_`n4+JO6?|^FZKGeaL|W6cT{_AukVi^ z&nfxJDuKjCe{a^+;lp3O*NoEF8^wR6+AkM=!TdM18ygA#CTrgMp_{TVFbBr)!-W}o z(-6e*-{kfqMR`K_z$F17MS^GIPg}*5Rkiy;(<5?S=xAwU!A*?R(Ieg+ajh6>oqzjH zSp7A={jH5oWx9d=4Y=C8?H;@j=%byu z0IQ4-zXG4Ln_i|X7?@uJvEbspJ>zO9z7OZYryM(PExCg)O@AphvqH{qSN;C(wqxwj zYZE9ErpvVGP;%Mxg}-IyXiM}OhiqrWeOjjW)+ay1YllImOCpo0(wvx+>a7i}XW1zF z*tQbvB!IfR^%0>0Hz?TOW7*5Hzp9(rBl=+6jy~yZFP={gKTdWa-U~eJ+DY;<2cPG{&&;o# z;na|gVhGFC{H@RJ4(t{a0jIv{Cckw=7v8?LAm~W3<0LTdPjva>_UiLA!2R# zo^*JY{b`o?->Uq#ErRq}DhIXxo{U5m@s_HPEVkm)2G5`X^LN-eUpxZKo_xMplD$y9 ztv~5Lhoi~)mrebP;nB;RGPLf-w?~xOSHI5#&Q^rsedz?=B8<8^wkm@ker*a+o>(tu zx`XF+IjrX;yv9y!=<#IxL<0_)Aexc1XLxYfBfi!2Mm&{uZ=ccpY z(BlZ2@m3izxhqB8*pU~Mi-Lb`Zf{4-3ng(VYxObJL$8Y#|4B%Y0aXsjjqn7bI6R zGvDD&054&RFH8KNov7Lf3Z5wK96ys;(LI!?vO4x02!~F#kDG&^W46EAp?r!yo}0$w zA1F@!KpJ~e2kZ6S)GL;4OCMt1rcV1{i`a>Z%=+*qigXi6_VkXl&d-&2(#)|aU6;tJ z<>I!`?hb6bUM41fF5Qe9e0Pkjxp^w;+lGarFof5&=c@FjpSoKj4&XJv+$w>vqWqE% z;YU-pz>8GRRp+Yu;ifa6%8M9<>)Wq}je5=o;|F;^;6Iz=2qsqW_3gPy(N8Zju<-(O zGBa9^dN$_We)o+T$gnV58+r<(nfC-?`;JLM3Em(?TqnlxiNbp+=I8g(4*4m-$>T9g zY{ja(ir!qgJ1=~p=6F@9pBje$#>gM=5&6q)%+1VuNfG-s_&&D>kbCa}0MYD5aszLA ztooP!sjA#AptOCFm&Gus=2(|UMgJi9&$ivzJ-#H+A zPiQY*ee8U)cGby!yFUf7B;@0bT*QrkswlqIfA1#xur00yH_!-CH{gYeusCs0uD{`G z**evXva<}>LIdMl?Jz%tpGES&sG^8EIQkA;lUs#xDs7c39iuH((iBr3*@&j(>QY;< zG`f80eFnYC#2nZ5IM7zq|LEkSyIu_!pC|5?V9GSCRe}jMk)B+#0y2?a3t^uF>++p%@c>1I*nX z6gh(s>v&3hWQqBBht_LDQ#VFT1K+<-_@wuYt-_rph+(c5cW|t|ldS-R14tpGwsX>2 zmtjg9kDqgtvPu(ec{kx43Y&G|>RRiY`6 z3W@f6A~BxLjX`g zi7w5q(WX5f%cT>-3_Y8|g&;F~hx7Ba;}|L#nL3?@@y&~wOHP3`Ocp(ggagS~FNsfU?~gUF^>>x}_6vS(ey|NMZbF`(t1T0QXm($6W|tTuqz^3kW) z`tr+h)Xa<=fKR&j$N&=0Xk;K0TU3k8wh#lP3$n$Ay(EkM{Kt*nxJBv(Xz75UW!Ec( z0UN0{VBG-^--4>$VbVqYm!goi62`fyTl0Z)ltGdCB>wL@c zoa@R3I|UL7{`qas9Yx)?FIiXPMqm6HOc6UftuUzS#YQ!<72~_&NzzjzqidI~Mwyss z0s+LlIm$b9ukYzOE76L3aXm_-01Q({%9p z^LTjFy0imVbDW|JDJNvrRTn&NoOZ5F6ZA1cP&KWJCj^V^q(21uWs-IyJFqbr5Gv%akR z7<=KSo5C8dXPxP^BG_?|+I``p%D^S!{a&Ew$(|VN_ihWCs_ROl?8$R=)4`(3?hu*X z^abA@1yfD`Z>9=nH12!~sc5eTB#$Yj32fYk9GH zucCiDk-bLs*qnjZx?Pk-&~rrPkF9O+IlL=jOv-*P$4(x0zl)yToQS#mx3u}TPNR(d zz38*^G}l{>zw?R*#FzYXB`K@}e9E+@1J(VooY#+y3*O3c+s}r({+d8RtDDkH>R;RM zKINepQ9B+do*9Mkw9~W%aHO2qkL`pyuwWK(El;GyPI!~!m1{Y2ZA|0cx|{US34DiU z%Uzu}_t~#9w^M<GB<0Xnb7xg0AAlI#CU`weXqNy@D5? zQ{m%-GTTfKPGPl@8>Nyf&|@B{X*g`;UcBP3&P6^(At1{S<`~6?mSWFIXgV|Vzvslc99c0+gmVl!L<4H zzntEDFd1(v7r5dTpB3^z)x5qAU<8jdKarXVw4JKKA1;ELDpQBPBJzDq6vE!v=0cJT z?0$%~XL^L^P47)wsZV|TBl`irK= z%_Pi)(*HDT@My>OgysD3{}^vhWyD_q_RT6^CdfMnm$X7?Bzu~h$gR=Ji$1_g>rBkN zE}!?aId^qRt4@5%x8`$oGLZME(7!3nrip=ZkYGv*pfP*<<)jAg(#BKg?eRvSRei(o zOzo9HM>f~JJ&b{rFpE!@zji)=CKL);y?HfaJcaAXH&<2o5_r?EpG1hr{&!fMNXppJ z=ePc&=vb`zs5h>vHVe%`xhDS#{82sE))#OJK3A`WYw1eoXoOxRhgky%f0272DnhFa@QEvCz=hp z3;eUTI&_`Tdhj-g!Pkr?M~n77`Qp60a(mh1OQkYf5{+lGA4}Iv4}@8qB(}w`^*Vb; z3|}>B0B-`*_oZI%kN^Jg!4CG}67!-BUV;<*b#mgnBbP5s#FFagX@y%U5z$O+-}I=) zl(~5!u3EOOWg{k1ENuV!JFCU2L2wc#W`6eRhwvI?@M{$fT8C4Is_2eIR^Ovz;}-~! zer!?siS9PTeLVp)c*Ln+2|9aw_ui3lC5rXkQKdwdOjqV1KFUN&>Su^R-#OS$K%HJ#|gW_<@ zh$XAeQM);|frc?OQHWeMc`?c2D@k4zSP61S>#R*C)5mzBaHPJ4x?6m1AyDFX>3~`t z(%tmcOaeWa&HfqHN3_ZUuO{MX$sHPD&%vVD7@Ns3io|u@Kb6#v>5=Yv!YC(^zJ79oXiPD$2q@@C(s9jvbmo zUT*3%K09GpFYRlQ|7CkxfvNzh?f6w8&XhdfNhjlt`A-yoK2qnZH_;m`kMI$N%H)&) zo46DuoEu-xy`0Qf|K2-bz$zV_#^$Mk?SSw2bzZtfO`}UDB#W!X z?=<^)^3|!OmbO#JYv`W`Jx@es{_D}E7C4yTR~1N;YB9JvFvkVj&$L-t^z8n)5EZMn zfN1PNDWnom)8WYqQvd8S&YOmX9e90RA;9e<|3nnbw8)I=WCYX|6|RKP@DB(yst92t ztyFCA#ophfnkyWg;yjwVpO|5frS~y>0eC4KnFgB#LzJFIikM1W6 zB{to)3%)5gE1a6#Qvvv|;&r+bhuEB>bo9TS$w0m{a+F+zl!d= zl@5qJMoQ;nL%~8`!RABZH-W)N`B_#L%1pGWX3q%kZ#sXLUjY^C z`Y8=iIdjXXL+EELQkLI*O3}`qRZ+F-G_i6bx4}U9tyG2>lAO33k32|}E0!Bm{>&D< zfaGTLu2_fK=pSFT_qH+UU{SM`fRC7da5nKTXYLZfQ3R3oIg*o|xu|F*^c`F7OWp5J zKa%zgr)kqAiZoT^U8TkztcNcw-rG%DBo|Zao3Ee*8KHMEl)KxL6-M=xbOTU6_-@Yo{*n2ed@n$3Q zkEP{l3U)xSji*x(ezLUWuS>o4`$A&D*-wiw!5%iBkYCdU`k`c`Umq1QcCb zK8jZLtOQ1*5{23IKwHz!0o5MaslD~KKYw1K;I`$mIrS5LcH zBUJKbynp45jPp`2_797$I8##IxUrz^+$5DVZ?JTt=w7KR0fkBKVdI3|?)pp0zk)Ke zrq0Kj=B1Fj1lFWltGUel3X~`K(B#`qx5I7X+rat^=NsR7$>{Yq`W92Bjc@K2;Yf2J zP+dM=0P373>rk~JTf2Xm(BMJusOVTWO#rjeYrZfm6U&+p+&D8=rnVWjPS3iE`{5zb zl2Y~}!tN#?OYPVWg{}71^puKcwSGS?X=myyEaM^jYXOAk^USB5Ij}UoGTeB8 zVepkLog2yTgG*znSMRg8e*CPcw3mwTtZbHZ;#s7+m>(emo6a<9sP>VW=FR_oR=^WaTe$4G>5BCl1o?ImJggr?8-S{}FXL35R3m^^NSMUjtF7uwL7e(RaK1%jI%j>)yA6EVSIojTK1dV)z{h^?;~E8+v}-e8 zHGW-~79r+1dr|P1*Dkbr1G?K6)P(%gWvB8e-hqVjTI0e|=_o`|uxqY9-yZHyhu$mlLz?*&lD zMK`5fQfKZOd5tsU^K<)YoZQD*u$u1Qc!H?xRHeZe&Y1oRkKT4Ux{z6M$&t~LJ$5)f z1plX1Ei1}J^H^b&BsmT(wQu9~jdijfm7=`DU#`w*tLxpQea1d7)o3y+UXiu_US!gB zp5y7H-oo;*T)S8&8N~BPC6nNkS2OtNC2oi1_&B=S(yKxNXl#|j^f>{yndInvFUmMj^OK4?QaL8-f#_;D(_T;u_=zWt`yTW718+U zuIdW-o%2V1sp}GpPae}?bxzMPySOeSB0Y~}XhXfU`5Y{4)&##tYPG;;k!D{kN0Phb zhj-tjUPZdRS=O35m=6LrV& z%Nk`y?+m2~D66`HYP_7*gXH0l#cn@x6n+LJCq6uU7fxcQIzPdi)qU;mc#NXg$5lK2 z8NRdiJabct3?>{reV;mklYz@{i)7Pp26RvH@COHI?S9?Yj3`+);Khf9QoJ@GS=Odx zZ&IpHaYFcgz^B37<0*2wNod#UqN7}*K*MYB-^CA}i5YV}`Buo?-eRQn;e4zfYMPea zW&$b_Jj%x4q-yN77t)=4(e}3zZ1`e86$DZ~!*1i|WGunpzd9o2=@vhtK>2pCz+a&) z!;V5FQ~1>eLwkm;yp!4we%%PsB6WRTOTWmUtSb8qY!zxlj<5D)08Tk)d->hbf3ixy zJhLz*rlogpOB*}(=x%SziJuGP zmhLv3?l}QBi1o_HA|_yLe;0iQpEg6HigjuPgQ16 z5LG9J#fRhU1DM|WMFizSF}um~j?%oVxfxr!Lshz!FHlrlQ_$g7!K`i z&+ZQx1EsO|f$>Z+!kKJZ(yxAc8MBs#N^zSCUVqkU*V)jVJ-$4{>~A+Z5LNy$=8{lU*p%?XVFO%Wyj8sc z^ATx43GMp}b7?BoDadA+%E9x&C2OWE))xmhGw<2xeg*s@OlJLiCH@T zF#r(x%~kIkC&v`8Td1o2LJWIXdZ+(-bYZWpM1c(#7b3eV=KWZRFZ1!d_HtJ4s{F0# z$*;g$VSAmti&a^yOsG3UfLWMui zxA=Fd+DECM(b?>;BIcWTEZvxinYqqEM#-P*9y=AM;-pCGIF^-3ZWuM^#MRZ=?QT;7 z09JrbR9eWK_HK%IpVaO&s`SPY zD^w6iP0B2`ZBo<$RnzBigNPx&NDQBU$lK|B1Xp?2hP=v`bPDF{AN{FnM3W+l?X=|u z{pIV#oK@qT{*e^iGJr*UiGAd+3~5|K$S<3vO3s1tdWPPC+9f*0wB!`cz8%tC=_E5N z{TuDKg7WMs?~HY+uW9xf+SL{v@)kh~aH9f2@I;_|AU!i$LZnko8m5`%O~YJN3LPZ}uc8#7`Bjp-N#hqZq;ob>ERwHc8^YcXaeRB>dScys~hmGsc(_IJ;eod+>>yT zH|GcGjtEt-;`n`1Z?A;x>mnDo%*(Z76J1QDX1*^x&cvLxyJy5qCmPNsSMoNldRH{0 zg7e|Wg=1LG({e#zU381|e-&L@+&$%wlwy?qpAJn)n>PAyzX(+uRHcxvu--7Ft~%wX zmOs#`%;v)oFMhtPg>uL~9Ae9dx&qVb6Zb=;(11KhSi3zqz#Axs`77RddIWB((`1-=b9i%C5e|XmkGq^7`WRe22@d@h>(M zz4A}J-)7S`L@huG10E*$a<~ml_DKVYc-KiuwzVAm3z`>S=@->t#aNyNrhaVd###%- z4BRVuDJj5S1uaN?`oO7i0!G~Hg}jppWMW#YoN>h{g7I3-Nw*b{f}2!E4m3aW$>$j= zSJt*+NoH-$EMT8qOzdLl>QPLuT#Grmtkogr;~>ua z8mv0>r=lWOwvXvKJG@eY4oT{Yv zO0=dQJD(rn#m0P3o|DT!kiRn;Dm3-eemqxqaY#e!ndl)^+{)ty0qw{SQ2Mm1i)Xp0 z-|E$#WUn$mue1XwEf(;t4g4b81=vr3rQ|8mgJ~CiLG_?4kA~eOsY+n_SvF67U~M~W z^+IPao&!9BO=CV`KBkjR)yeVrB_1oF}s5)ktXRa}XyEr#f2L=eA`fzT@F_ z=_%-}XwTCYyf&X+;zlOU60oi)u(Duh5k9qU_hnS?+EI%i6DL*8ltZR!TT35e3#6(8a?V5a@$TC~xhU)Kgi zuFiyF_Nrx0EC?-jd{i-zI1qRZo(w6ZJxM@^CfAGa(1&3IB8|nSJfbssHrG4^ zn99#^v(i>p=h+FCg1xO{ZTS*yttq32yF5poKGh(?9qjOkFJSMdmnhp6F zZ2ae)fGW@XxR-xFi0blG-k$906NJZz$#w*@=Y4GA(-)DzfFm?WYJZV@lSz^F927DC z`#g6?2`O>|Rsc4adNw$wz2<1`_6$giY_Z91=u{}K+Vy{|D!X8LyMaGCykq!6jHjY$l$G?j}2kh5#j!9_imp~h@4 zGZ0HflFn}ypqJ%EKXi{#28?C4Va}ANKKk%WZ9#|qMjIHN@iCVfpeShGq3e{uVHe8G z?W0eqPTj6jVU6Kf?QSp#ETWqC95 zN_^3SByMUA4HO{cmVDKSADvmX2l~(h&SUl>H;0QU-{=eP&U-Anu^oZFT!ftFe!PKt zGLFmZit^|z%04AGm-Xgt8b|+}JfWL~`1)gf-r7EO@g) z1oQ@<+YplH0;}{>#ve!^z<_xk#~ao?DA0u)mDH33_r{$|V{g*{FN)CN>$VLNqLf~W zS(RGq%+tg1mZ18O&C1rl&@yUKy#g)~WLuL~iZSKQ+5zX$(S{zhJ#`HzEf_O$wNLOErV31Op_D;bk z=xV*(7U`r;8L)f#HDUl<5pZc^YsK##?*6buH*3Ad*T9Es0~&Z`I84R7Zv343uVP47f`5cz8^ zH6a-`c4@x69!Yp=H-~#k71Ua`YXG*kJTcy!5EhRKG>J950Mon;1`tN$-l?J0@M*}F zb$t`nlc6@d^VP}KmUEjRWNJx4@@61cOJ%7F2G-Ol?kMNYpHee zSkMsXmAzo( zQ2IUJfdBO4#`m=LB7S?Xai;qw!bF|XI2JWEYBx&?0S`y&8Yk=+nNSX!$BvWL-<7SPr|bDP7x+-HY2M6|ApvdKi~ zsQpBX;BD&qhlZ_KS!RbLXJcC7>YwjX zEqYZAQ^y2*#_kP%80*bR|3Lgo@gAWFqqZkD8FcWRDYJ}&napDW-UkXCC&xFkMnJDq zle~Bjo_jNRlik#>Gb0^3)#>ew^0g|bL;|L7Ni+tDCi6Lf4*IL8HzJiANk;}qkx#ey zd0PekS83GIUxXZ3HiGzyl{;@eYB-fZZY-Sv8Ii`TG#S%Qi!;QS&Ypwb3%vP8uiq`^ zq-kpIB+B@?Z~@4Xvy4SRpTNeM%gZ3Cl#mtJAt6X$jj5{9C$b6>)?{(WDQ*rI4uOZ} zgt!P(2~g#$8NfC@-6-|f%_k6551mPrQ<9ETYg!isN>TEtUW7$QNcuEz5 z2nIE|vFwl)nvAf0zST4NL)r*+xywt>=!wsAhrbZUdD5Fdo-u;0uZ|@&)5Mk8ze16Q)LIzhJM8T_~CPY^I?hEA%6F3 z7T*_}cFwIfAR8`XfvccCtzbpp9e9$lSPoJ@jish{e(E%#*P)|+|DDY7p`631pf->z z8-s35d?-$a1BnYZcOgn`^TQ>H$Kd1&<%S zAKQhhL>|@|jW7XEzf3BwY4{O-QV-iSOB<6QHp=lv+NhqZAVxXZqQoGb?NJD9Zs9eS zxE(H_8B6?a^KI1M|A&DiKl|kLUBu%pKKZ-tMWKoW8&S|`&GcGQWvQP<4OT?f_ay1r zRv^uP&rKoFDkRp*cRMKs#2ZZ~s&ywa+h8!K@tA13cXw>jlTwA9N0I-^vxP(c+il%B zF>CYr6L(Krbuj2C4Y(Kr-3PeubZSFfi7XA{T*G&JRPft}gvB|MpaSq+<^Lcheck_circle | -| src_nk | Source natural key column | String | List | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_pk | Target primary key column | List | List | check_circle | -| tgt_nk | Target natural key column | List | List | check_circle | -| tgt_ldts | Target loaddate timestamp column | List | List | check_circle | -| tgt_source | Name of the column which will contain the source ID | List | List | check_circle | -| source | Staging model reference or table name | List | List | check_circle | - +| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | +| ------------- | --------------------------------------------------- | -------------------- | --------------- | ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | String | check_circle | +| src_nk | Source natural key column | String | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | +| tgt_nk | Target natural key column | List/Reference | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | +| source | Staging model reference or table name | List | List | check_circle | + #### Usage ``` yaml tab="Single-Source" -hub_customer.sql: - -{{- config(...) -}} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_nk = 'CUSTOMER_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} -{%- set tgt_nk = [src_nk, 'VARCHAR(38)', src_nk] -%} -{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} -{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} +-- hub_customer.sql: + +{{- config(...) -}} + +{%- set source = [ref('stg_customer_hashed')] -%} + . +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_nk = 'CUSTOMER_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_nk = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} ``` ``` yaml tab="Union" -hub_parts.sql: +-- hub_parts.sql: -{{- config(...) -}} +{{- config(...) -}} -{%- set src_pk = ['PART_PK', 'PART_PK', 'PART_PK'] -%} -{%- set src_nk = ['PART_ID', 'PART_ID', 'PART_ID'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} +{%- set source = [ref('stg_parts_hashed'), + ref('stg_supplier_hashed'), + ref('stg_lineitem_hashed')] -%} -{%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} -{%- set tgt_nk = [src_nk[0], 'NUMBER(38,0)', src_nk[0]] -%} -{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} -{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} - -{%- set source = [ref('stg_parts_hashed'), - ref('stg_supplier_hashed'), - ref('stg_lineitem_hashed')] -%} - - -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} +{%- set src_pk = 'PART_PK' -%} +{%- set src_nk = 'PART_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_nk = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} ``` @@ -132,80 +155,77 @@ ___ Creates a link with provided metadata. ```mysql -dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, +dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) + source) ``` #### Parameters | Parameter | Description | Type (Single-Source) | Type (Union) | Required? | | ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | List | check_circle | -| src_fk | Source foreign key column | List | List | check_circle | +| src_pk | Source primary key column | String | String | check_circle | +| src_fk | Source foreign key column(s) | List | List | check_circle | | src_ldts | Source loaddate timestamp column | String | String | check_circle | | src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_pk | Target primary key column | List | List | check_circle | -| tgt_fk | Target foreign key column | List | List | check_circle | -| tgt_ldts | Target loaddate timestamp column | List | List | check_circle | -| tgt_source | Name of the column which will contain the source ID | List | List | check_circle | +| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | +| tgt_fk | Target foreign key column | List/Reference | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | | source | Staging model reference or table name | List | List | check_circle | #### Usage ``` yaml tab="Single-Source" -link_customer_nation.sql: - -{{- config(...) -}} - -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = [src_pk, 'BINARY(16)', src_pk] -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} - -{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} -{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} - -{%- set source = [ref('stg_crm_customer_hashed')] -%} - -{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} -``` +-- link_customer_nation.sql: + +{{- config(...) -}} + +{%- set source = [ref('stg_crm_customer_hashed')] -%} + +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} +``` ``` yaml tab="Union" -link_customer_nation_union.sql: - -{{- config(...) -}} - -{%- set src_pk = ['CUSTOMER_NATION_PK', 'CUSTOMER_NATION_PK', 'CUSTOMER_NATION_PK'] -%} - -{%- set src_fk = [['CUSTOMER_PK', 'NATION_PK'], ['CUSTOMER_PK', 'NATION_PK'], - ['CUSTOMER_PK', 'NATION_PK']] -%} - -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = [src_pk[0], 'BINARY(16)', src_pk[0]] -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} +-- link_customer_nation_union.sql: -{%- set tgt_ldts = [src_ldts, 'DATE', src_ldts] -%} -{%- set tgt_source = [src_source, 'VARCHAR(15)', src_source] -%} +{{- config(...) -}} {%- set source = [ref('stg_sap_customer_hashed'), ref('stg_crm_customer_hashed'), - ref('stg_web_customer_hashed')] -%} + ref('stg_web_customer_hashed')] -%} -{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} ``` @@ -263,67 +283,67 @@ ___ Creates a satellite with provided metadata. ```mysql -dbtvault.sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, +dbtvault.sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - src_table, source) + tgt_eff, tgt_ldts, tgt_source, + source) ``` #### Parameters -| Parameter | Description | Type | Required? | -| ------------- | --------------------------------------------------- | ---------------------| ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_hashdiff | Source hashdiff column | String | check_circle | -| src_payload | Source payload column(s) | List | check_circle | -| src_eff | Source effective from column | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| tgt_pk | Target primary key column | List | check_circle | -| tgt_hashdiff | Target hashdiff column | List | check_circle | -| tgt_payload | Target payload column | List | check_circle | -| tgt_eff | Target effective from column | List | check_circle | -| tgt_ldts | Target loaddate timestamp column | List | check_circle | -| tgt_source | Name of the column which will contain the source ID | List | check_circle | -| source | Staging model reference or table name | List | check_circle | +| Parameter | Description | Type | Required? | +| ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | check_circle | +| src_hashdiff | Source hashdiff column | String | check_circle | +| src_payload | Source payload column(s) | List | check_circle | +| src_eff | Source effective from column | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | check_circle | +| tgt_hashdiff | Target hashdiff column | List/Reference | check_circle | +| tgt_payload | Target payload column | List/Reference | check_circle | +| tgt_eff | Target effective from column | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | +| source | Staging model reference or table name | List/Reference | check_circle | #### Usage ``` yaml -sat_customer_details.sql: - -{{- config(...) -}} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} -{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} - -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} - -{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} - -{%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], - [ src_payload[1], 'DATE', 'DOB'], - [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} - -{%- set tgt_eff = ['EFFECTIVE_FROM', 'DATE', 'EFFECTIVE_FROM'] -%} -{%- set tgt_ldts = ['LOADDATE', 'DATE', 'LOADDATE'] -%} -{%- set tgt_source = ['SOURCE', 'VARCHAR(15)', 'SOURCE'] -%} - -{%- set source = [ref('stg_customer_details_hashed')] -%} - -{{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - source) }} +-- sat_customer_details.sql: + +{{- config(...) -}} + +{%- set source = [ref('stg_customer_details_hashed')] -%} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} +{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} + +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} + +{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} + +{%- set tgt_payload = [[src_payload[0], 'VARCHAR(60)', 'NAME'], + [src_payload[1], 'DATE', 'DOB'], + [src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} + +{%- set tgt_eff = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source, + source) }} ``` @@ -417,7 +437,7 @@ CAST(MD5_BINARY(CONCAT( ``` !!! success "Column sorting" - You do not need to worry about providing the columns in any particular order as long as you set the + You do not need to worry about providing the columns in any particular order, as long as you set the ```sort``` flag to true when creating hashdiffs. ___ @@ -444,7 +464,8 @@ column AS alias ```yaml {{ dbtvault.add_columns(source('MYSOURCE', 'MYTABLE'), [('CURRENT_DATE()', 'EFFECTIVE_FROM'), - ('!STG_CUSTOMER', 'SOURCE')]) }} + ('!STG_CUSTOMER', 'SOURCE'), + ('OLD_CUSTOMER_PK', 'CUSTOMER_PK']) }} ``` #### Output @@ -452,34 +473,39 @@ column AS alias ```mysql , CURRENT_DATE() AS EFFECTIVE_FROM, -'STG_CUSTOMER' AS SOURCE +'STG_CUSTOMER' AS SOURCE, +OLD_CUSTOMER_PK AS CUSTOMER_PK ``` -#### Notes +#### Specific usage notes + +##### Getting columns from the source +The ```add_columns``` macro will automatically select all columns from the optional ```source_table``` reference, +if provided. + +##### Overring source columns + +You may wish to override some of the source columns with different values. To replace the ```SOURCE``` +or ```LOADDATE``` column value, for example, then you must provide the column name +that you wish to override as the alias in the pair. + +##### Functions + +Database functions may be used, for example ```CURRENT_DATE()```, to set the current date as the value of a column, as on +```line 2``` of the usage example. ##### Adding constants With the ```add_columns``` macro, you may provide constants. These are additional 'calculated' columns created from hard-coded values. To achieve this, simply provide the constant with a ```!``` in front of the desired constant, -and the macro will do the rest. See line 3 above, and the output it gives. +and the macro will do the rest. See ```line 3``` of the usage example above, and the output it gives. +##### Aliasing columns -##### Getting columns from the source -The ```add_columns``` macro will automatically select all columns from the provided source reference. -If you need to override any of these columns and provide different values, for example a different ```SOURCE``` -or ```LOADDATE``` column value, then you may. If you provide columns in the ```pairs``` parameter, then they will -automatically take precedence over any columns coming from the source. - -Database functions may be used, for example ```CURRENT_DATE()```, to set the current date as the value of a column, as in the -example below: - -```sql -{{ dbtvault.add_columns(source_table, - [('!TPCH', 'SOURCE'), - ('CURRENT_DATE()', 'EFFECTIVE_FROM')]) }} -``` +As of release 0.3, columns must now be aliased prior to loading, in the staging layer. This can be done by providing the +column name you wish to alias as the first argument in a pair, and providing the alias for that column as the second argument. +This process can be observed on ```line 4``` of the usage example above. - ___ ### from diff --git a/docs/roadmap.md b/docs/roadmap.md index 5b8b5ed93..b610c0880 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,52 +1,31 @@ -With each release we will be adding more Data Vault 2.0 tables and helpful macros. +With each release we will be adding more Data Vault 2.0 table templates, helpful macros and productivity enhancements. We hope to tailor new features to the requirements of our community, making the package the best and most useful it can be. -We will be releasing changes incrementally rather than jumping from Release 1 to Release 2 and beyond, so you can reap -the benefits as soon as features are developed. +We will be releasing changes incrementally, so you can reap the benefits as soon as features are developed. #### Contribute to dbtvault - Do you have some ideas? [Let us know what you want added](https://github.com/Datavault-UK/dbtvault/issues) - Want to contribute your own work? [Read our contribution guidelines](https://github.com/Datavault-UK/dbtvault/blob/master/CONTRIBUTING.md) -## Release 1.0 +## Coming soon -We're currently working towards release 1! +These features are currently planned for the near-future. -Everything is ready to go and can be used as it is, we're just cleaning up some of the rough edges and making sure the -documentation is up to scratch. - -Release 1 will include: - -#### Tables - -- Staging -- Hubs -- Link -- Satellite - -#### Supporting Macros - -- cast -- hash -- prefix - -#### Planned improvements - -- Make providing aliases and types optional when defining target metadata in table template macros. +- Full Snowflake TPC-H Demonstration to supplement the documentation +- Transactional Links (Also known as non-historised links) -!!! success "New in v0.2-pre:" - - Removed the need to add columns which already exist in raw staging. - [add_columns](macros.md#add_columns) is now only requires entry of metadata for calculated columns or other user-defined additions. - - Removed of the ```tgt_cols``` parameter in the table templates, as this was duplication of metadata. - - Hashing now alpha-sorts columns automatically. +## Future releases -## Release 2.0 +In future releases, we hope to include the following: -#### Tables +### Tables -- Transactional Links (Also known as non-historised links) -- PITs -- Bridges -- And more \ No newline at end of file +- Multi-active satellites +- Effectivity satellites +- Status tracking satellites +- Point-in-Time tables (also know as PITs) +- Bridge tables +- Reference Tables +- And more! \ No newline at end of file diff --git a/docs/satellites.md b/docs/satellites.md index fdefaba9f..0cd252e8d 100644 --- a/docs/satellites.md +++ b/docs/satellites.md @@ -1,4 +1,6 @@ -Satellites compliment hubs and links, providing more concrete data and temporal attributes. +Satellites contain point-in-time payload data related to their parent hub or link records. +Each hub or link record may have one or more child satellite records, allowing us to record changes in +the data as they happen. They will usually consist of the following columns: @@ -12,15 +14,21 @@ the hashdiff will change as a result of the payload changing. a name, a date of birth, nationality, age, gender or more. The payload will contain some or all of the concrete data for an entity, depending on the purpose of the satellite. -4. An effectivity date. Usually called ```EFFECTIVE_FROM```, this column is the key temporal attribute of a -satellite record. The main purpose of this column is to record that a record is valid at a specific point in time. +4. An effectivity date. Usually called ```EFFECTIVE_FROM```, this column is the business effective date of a +satellite record. It records that a record is valid from a specific point in time. If a customer changes their name, then the record with their 'old' name should no longer be valid, and it will no longer -have the most recent ```EFFECTIVE_FROM```. +have the most recent ```EFFECTIVE_FROM``` value. 5. The load date or load date timestamp. This identifies when the record was first loaded into the vault. 6. The source for the record. +!!! note + ```LOADDATE``` is the time the record is loaded into the database. ```EFFECTIVE_FROM``` is different and may hold a + different value, especially if there is a batch processing delay between when a business event happens and the + record arriving in the database for load. Having both dates allows us to ask the questions 'what did we know when' + and 'what happened when' using the ```LOADDATE``` and ```EFFECTIVE_FROM``` date accordingly. + ### Creating the model header Create a new dbt model as before. We'll call this one ```sat_customer_details```. @@ -34,23 +42,40 @@ The following header is what we use, but feel free to customise it to your needs Satellites are always incremental, as we load and add new records to the existing data set. -An incremental materialisation will optimize our load in cases where the target table (in this case, ```sat_customer_details```) -already exists and already contains data. This is very important for tables containing a lot of data, where every ounce -of optimisation counts. - [Read more about incremental models](https://docs.getdbt.com/docs/configuring-incremental-models) ### Adding the metadata Let's look at the metadata we need to provide to the [sat_template](macros.md#sat_template) macro. +#### Source table + +The first piece of metadata we need is the source table. This step is easy, as in this example we created the +new staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. +dbt ensures dependencies are honoured when defining the source using a reference in this way. + +[Read more about the ref function](https://docs.getdbt.com/docs/ref) + +```sat_customer_details.sql``` +```sql hl_lines="3" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} + +{%- set source = [ref('stg_customer_hashed')] -%} +``` + +!!! note + Make sure you surround the ref call with square brackets, as shown in the snippet + above. + + #### Source columns +Next, we define the columns which we would like to bring from the source. Using our knowledge of what columns we need in our ```sat_customer_details``` table, we can identify columns in our staging layer which map to them: -1. A primary key, which is a hashed natural key. The ```CUSTOMER_PK``` we created earlier in the [staging](staging.md) section -is a perfect fit. +1. The primary key of the parent hub or link table, which is a hashed natural key. +The ```CUSTOMER_PK``` we created earlier in the [staging](staging.md) section will be used for ```sat_customer_details```. 2. A hashdiff. We created ```CUSTOMER_HASHDIFF``` in [staging](staging.md) earlier, which we will use here. 3. Some payload columns: ```CUSTOMER_NAME```, ```CUSTOMER_DOB```, ```CUSTOMER_PHONE``` which should be present in the raw staging layer via an [add_columns](macros.md#add_columns) macro call. @@ -61,9 +86,11 @@ raw staging layer via an [add_columns](macros.md#add_columns) macro call. We can now add this metadata to the model: ```sat_customer_details.sql``` -```sql hl_lines="3 4 5 7 8 9" +```sql hl_lines="5 6 7 9 10 11" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} +{%- set source = [ref('stg_customer_hashed')] -%} + {%- set src_pk = 'CUSTOMER_PK' -%} {%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} {%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} @@ -80,9 +107,11 @@ provide the metadata it requires. We can define which source columns map to the define a column type at the same time: ```sat_customer_details.sql``` -```sql hl_lines="11 12 13 14 15 17 18 19" +```sql hl_lines="13 14 15 16 17 19 20 21" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} +{%- set source = [ref('stg_customer_hashed')] -%} + {%- set src_pk = 'CUSTOMER_PK' -%} {%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} {%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} @@ -91,16 +120,15 @@ define a column type at the same time: {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} -{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} -{%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], - [ src_payload[1], 'DATE', 'DOB'], - [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} - -{%- set tgt_eff = ['EFFECTIVE_FROM', 'DATE', 'EFFECTIVE_FROM'] -%} -{%- set tgt_ldts = ['LOADDATE', 'DATE', 'LOADDATE'] -%} -{%- set tgt_source = ['SOURCE', 'VARCHAR(15)', 'SOURCE'] -%} +{%- set tgt_pk = source -%} +{%- set tgt_hashdiff = [src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} +{%- set tgt_payload = [[src_payload[0], 'VARCHAR(60)', 'NAME'], + [src_payload[1], 'DATE', 'DOB'], + [src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} +{%- set tgt_eff = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} ``` With these 6 additional lines, we have now informed the macro how to transform our source data: @@ -109,8 +137,9 @@ With these 6 additional lines, we have now informed the macro how to transform o We are removing the ```CUSTOMER``` prefix, as this satellite is specifically for customer details and it's superfluous. Renaming will always depend on your specific project and context, however. -- We have provided a type in the mapping so that the type is explicitly defined. For now, this is not optional, but -in future releases we will simplify this for scenarios where we want the data type or column name to remain unchanged. +- For the rest of the ```tgt``` metadata, we do not wish to rename columns or change +any data types, so we are simply using the ```source``` reference as shorthand for keeping the columns the same as +the source. !!! info There is nothing to stop you entering invalid type mappings in this step (i.e. trying to cast an invalid date format to a date), @@ -118,44 +147,6 @@ in future releases we will simplify this for scenarios where we want the data ty You will soon find out, however, as dbt will issue a warning to you. No harm done, but save time by providing accurate metadata! -#### Source table - -The last piece of metadata we need is the source table. This step is easy, as in this example we created the -new staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. -dbt ensures dependencies are honoured when defining the source using a reference in this way. - -[Read more about the ref function](https://docs.getdbt.com/docs/ref) - -```sat_customer_details.sql``` -```sql hl_lines="21" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} -{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} - -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} -{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} -{%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], - [ src_payload[1], 'DATE', 'DOB'], - [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} - -{%- set tgt_eff = ['EFFECTIVE_FROM', 'DATE', 'EFFECTIVE_FROM'] -%} -{%- set tgt_ldts = ['LOADDATE', 'DATE', 'LOADDATE'] -%} -{%- set tgt_source = ['SOURCE', 'VARCHAR(15)', 'SOURCE'] -%} - -{%- set source = [ref('stg_customer_hashed')] -%} - -``` - -!!! note - Make sure you surround the ref call with square brackets, as shown in the snippet - above. - ### Invoking the template Now we bring it all together and call the [sat_template](macros.md#sat_template) macro: @@ -164,6 +155,8 @@ Now we bring it all together and call the [sat_template](macros.md#sat_template) ```sql hl_lines="23 24 25 26 27" {{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} +{%- set source = [ref('stg_customer_hashed')] -%} + {%- set src_pk = 'CUSTOMER_PK' -%} {%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} {%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} @@ -172,17 +165,15 @@ Now we bring it all together and call the [sat_template](macros.md#sat_template) {%- set src_ldts = 'LOADDATE' -%} {%- set src_source = 'SOURCE' -%} -{%- set tgt_pk = [src_pk , 'BINARY(16)', src_pk] -%} +{%- set tgt_pk = source -%} {%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} {%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], [ src_payload[1], 'DATE', 'DOB'], [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} -{%- set tgt_eff = ['EFFECTIVE_FROM', 'DATE', 'EFFECTIVE_FROM'] -%} -{%- set tgt_ldts = ['LOADDATE', 'DATE', 'LOADDATE'] -%} -{%- set tgt_source = ['SOURCE', 'VARCHAR(15)', 'SOURCE'] -%} - -{%- set source = [ref('stg_customer_hashed')] -%} +{%- set tgt_eff = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} {{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, diff --git a/docs/staging.md b/docs/staging.md index e5c07ac1f..a4dd66c27 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -1,21 +1,37 @@ ![alt text](./assets/images/staging.png "Staging from a raw table to the raw vault") The dbtvault package assumes you've already loaded a Snowflake database staging table with raw data -from a source system or feed. +from a source system or feed (the 'raw staging layer'). There are a few conditions that need to be met for the dbtvault package to work: - All records are for the same ```load_datetime``` - The table is truncated & loaded with data for each load cycle -The raw staging table needs to be pre processed to add extra columns of data to make it ready to load to the raw vault. +Instead of truncating and loading, you may also build a view over the table to filter out the right records and load +from the view. + +The raw staging table needs to be pre-processed to add extra columns of data to make it ready to load to the raw vault. Specifically, we need to add primary key hashes, hashdiffs, and any implied fixed-value columns (see the diagram). +We also need to ensure column names align with target hub or link tables. + !!! info - - Hashing of primary keys is optional in Snowflake - - Natural keys alone can be used - - We've implemented hashing as the only option, for now - - A non-hashed version will be added in future releases + Hashing of primary keys is optional in Snowflake and natural keys alone can be used in place of hashing. + + We've implemented hashing as the only option for now, though a non-hashed version will be added in future releases. + +## Creating the model + +To prepare our raw staging layer for loading the vault, we can create a dbt model and call dbtvault staging macros with +provided metadata. + +Our model will consist of: + +- a header +- a source table declaration +- metadata passed to staging macros. +- a footer ### Creating the model header @@ -32,11 +48,11 @@ Let's start by adding the model header to the file: ``` -This is a simple header block. You may add tags if necessary, the important parts are the materialization type and -our schema name: +This is a simple header block. You may add further tags if necessary, for your own needs, the important parts are the +materialization type and the schema name: - The ```materialized``` parameter defines how our table will be materialised in our database. -Usually we want hashing layers to be views. +Usually we want hashing layers to be views, as they build upon the raw staging layer. - The ```schema``` parameter is the name of the schema where this staging table will be created. ### Setting the source table @@ -45,7 +61,9 @@ Next we will create a variable which holds a reference to the raw source table, in our model. !!! note - If you have not yet set up sources in your dbt configuration please refer [here](gettingstarted.md#setting-up-sources). + On line 3 below we are using a dbt source. + + If you have not yet set up sources in your dbt configuration please refer to [setting up sources](gettingstarted.md#setting-up-sources). ```stg_customer_hashed.sql``` @@ -56,16 +74,14 @@ in our model. {%- set source_table = source('MYSOURCE', 'stg_customer') -%} ``` - - ### Adding the metadata Now we get into the core component of staging: the metadata. -The metadata consists of the column names we want to hash, and the alias for our new -column containing the hash representation. +The metadata consists of the column names we want to use in our hash, to use as primary keys in our data vault or to use as +hashdiffs for satellites (see the DV 2.0 book for detail of what these are) and the alias for our new hash column. We need to call the [multi_hash](macros.md#multi_hash) macro and provide the appropriate parameters. The macro takes -our provided column names and generates all of the necessary SQL for us. More on how to use this macro is +our provided lists of columns, iterates through each of them, and generates all of the necessary SQL to create the hash for us. More on how to use this macro is provided in the link above. After adding the macro call, our model will now look something like this: @@ -94,7 +110,7 @@ This call will: value. - Hash the ```NATION_ID``` column, and create a new column called ```NATION_PK``` containing the hash value. -- Concatenate the values in the ```CUSTOMER_ID``` and ```NATION_ID``` columns and hash them, creating a new +- Concatenate the values in the ```CUSTOMER_ID``` and ```NATION_ID``` columns and hash them in the order supplied, creating a new column called ```CUSTOMER_NATION_PK``` containing the hash of the combination of the values. - Concatenate the values in the ```CUSTOMER_ID```, ```CUSTOMER_NAME```, ```CUSTOMER_PHONE```, ```CUSTOMER_DOB``` columns and hash them, creating a new column called ```CUSTOMER_NATION_PK``` containing the hash of the @@ -117,14 +133,14 @@ the primary key. We can also override any columns coming in from the source, with different data. We may want to do this if a source column already exists in the raw stage and the values aren't appropriate. -We provide the constant by adding a ```!``` to the data and alias them with the same name as the column we want to +We provide the constant by adding an ```!``` to the data and alias them with the same name as the column we want to override. You will have another opportunity to rename these columns, as well as cast them to different data types later when creating the raw vault tables. We can also use this method to create any new columns which do not already exist in the source. ```stg_customer_hashed.sql``` -```sql hl_lines="11 12 13" +```sql hl_lines="12 13 14" {{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} @@ -134,7 +150,8 @@ exist in the source. ('NATION_ID', 'NATION_PK'), (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), (['CUSTOMER_ID', 'CUSTOMER_NAME', - 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], 'CUSTOMER_HASHDIFF')]) -}}, + 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], + 'CUSTOMER_HASHDIFF', true)]) -}}, {{ dbtvault.add_columns(source_table, [('!1', 'SOURCE'), @@ -142,50 +159,41 @@ exist in the source. ``` -!!! success "New" - We are now no longer required to provide columns which already exist in the source table, - as providing the ```source_table``` parameter in ```add_columns``` will now bring in all the columns - for us. - - -In the example above we have have: +In summary, above we have: - Added a header (line 1). - Set the source_table variable to our raw staging table (line 3). -- Defined some hashing to create primary keys (lines 5-7). -- Brought in all of the raw staging table's columns (line 9). -- Added a ```SOURCE``` column with the constant value ```1``` (line 10). +- Defined some hashing to create primary keys and a hashdiff (lines 5-10). +- Brought in all of the raw staging table's columns (line 12). +- Added a ```SOURCE``` column with the constant value ```1``` (line 13). - Added an ```EFFECTIVE_FROM``` column which uses the ```LOADDATE``` value as its value (line 11). ### Adding the footer -!!! success "New" - The ```staging_footer``` macro has been renamed to ```from``` and is now much simpler. - If you're looking for the ability to add constants for ```source``` and ```loaddate```, - you can now use the improved [add_columns](macros.md#add_columns) macro. - - -Now we just need to provide the variable we created earlier, as a parameter to the [from](macros.md#from) +Now we just need to provide the ```source_table``` variable we created earlier, as a parameter to the [from](macros.md#from) macro. After adding the footer, our completed model should now look like this: ```stg_customer_hashed.sql``` -```sql hl_lines="13" +```sql hl_lines="16" -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} +{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} -{%- set source_table = source('MYSOURCE', 'stg_customer') -%} +{%- set source_table = source('MYSOURCE', 'stg_customer') -%} {{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), ('NATION_ID', 'NATION_PK'), - (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK')]) -}}, + (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), + (['CUSTOMER_ID', 'CUSTOMER_NAME', + 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], + 'CUSTOMER_HASHDIFF', true)]) -}}, {{ dbtvault.add_columns(source_table, - [('LOADDATE', 'EFFECTIVE_FROM'), - ('!1', 'SOURCE')]) }} + [('!1', 'SOURCE'), + ('LOADDATE', 'EFFECTIVE_FROM')]) }} -{{ dbtvault.from(source_table) }} +{{ dbtvault.from(source_table) }} ``` diff --git a/macros/internal/check_relation.sql b/macros/internal/check_relation.sql new file mode 100644 index 000000000..439e8d936 --- /dev/null +++ b/macros/internal/check_relation.sql @@ -0,0 +1,23 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro check_relation(obj) -%} + +{%- if not (obj is mapping and obj.get('metadata', {}).get('type', '').endswith('Relation')) -%} + {{ return(false) }} +{%- else -%} + {{ return(true) }} +{%- endif -%} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/create_tgt_cols.sql b/macros/internal/create_tgt_cols.sql new file mode 100644 index 000000000..d23e4d1ef --- /dev/null +++ b/macros/internal/create_tgt_cols.sql @@ -0,0 +1,100 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro create_tgt_cols() -%} + +{%- set tgt_pk = kwargs['tgt_pk']|default(None, true) -%} +{%- set tgt_nk = kwargs['tgt_nk']|default(None, true) -%} +{%- set tgt_fk = kwargs['tgt_fk']|default(None, true) -%} +{%- set tgt_payload = kwargs['tgt_payload']|default(None, true) -%} +{%- set tgt_hashdiff = kwargs['tgt_hashdiff']|default(None, true) -%} +{%- set tgt_eff = kwargs['tgt_eff']|default(None, true) -%} +{%- set tgt_ldts = kwargs['tgt_ldts']|default(None, true) -%} +{%- set tgt_source = kwargs['tgt_source']|default(None, true) -%} + +{%- set src_pk = kwargs['src_pk']|default(None, true) -%} +{%- set src_nk = kwargs['src_nk']|default(None, true) -%} +{%- set src_fk = kwargs['src_fk']|default(None, true) -%} +{%- set src_payload = kwargs['src_payload']|default(None, true) -%} +{%- set src_hashdiff = kwargs['src_hashdiff']|default(None, true) -%} +{%- set src_eff = kwargs['src_eff']|default(None, true) -%} +{%- set src_ldts = kwargs['src_ldts']|default(None, true) -%} +{%- set src_source = kwargs['src_source']|default(None, true) -%} + +{%- set source = kwargs['source']|default(None, true) -%} + +{%- set tgt_cols_dict = {'tgt_pk': (src_pk, tgt_pk, dbtvault.check_relation(tgt_pk[0])), + 'tgt_nk': (src_nk, tgt_nk, dbtvault.check_relation(tgt_nk[0])), + 'tgt_fk': (src_fk, tgt_fk, dbtvault.check_relation(tgt_fk[0])), + 'tgt_payload': (src_payload, tgt_payload, dbtvault.check_relation(tgt_payload[0])), + 'tgt_hashdiff': (src_hashdiff, tgt_hashdiff, dbtvault.check_relation(tgt_hashdiff[0])), + 'tgt_eff': (src_eff, tgt_eff, dbtvault.check_relation(tgt_eff[0])), + 'tgt_ldts': (src_ldts, tgt_ldts, dbtvault.check_relation(tgt_ldts[0])), + 'tgt_source': (src_source, tgt_source, dbtvault.check_relation(tgt_source[0]))} -%} + +{%- set tgt_cols_output = {'tgt_pk': '', + 'tgt_nk': '', + 'tgt_fk': '', + 'tgt_payload': '', + 'tgt_hashdiff': '', + 'tgt_eff': '', + 'tgt_ldts': '', + 'tgt_source': ''} -%} + +{%- set src_cols_list = dbtvault.get_col_list([src_pk, src_nk, src_fk, + src_payload, src_hashdiff, src_eff, + src_ldts, src_source] | reject("none") | list) -%} + +{%- set columns = adapter.get_columns_in_relation(source[0]) -%} +{%- set column_names = columns | map(attribute='name') | list -%} + +{%- for col in tgt_cols_dict -%} + + {%- set src_cols = tgt_cols_dict[col][0] -%} + {%- set tgt_col = tgt_cols_dict[col][1] -%} + {%- set is_relation = tgt_cols_dict[col][2] -%} + {%- set tgt_col_list = [] -%} + + {%- if is_relation -%} + + {#- Add column triples to list -#} + {%- if src_cols is iterable and src_cols is not string -%} + {%- for src_col in src_cols -%} + {%- if src_col in column_names -%} + {%- set col_type = columns | selectattr('name', "equalto", src_col) | map(attribute='data_type') | list | default(" ", true) -%} + + {%- set _ = tgt_col_list.append([src_col, col_type[0], src_col]) -%} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {%- set col_type = columns | selectattr('name', "equalto", src_cols) | map(attribute='data_type' ) | list | default(" ", true) -%} + + {%- set _ = tgt_col_list.append([src_cols, col_type[0], src_cols]) -%} + {%- endif -%} + + {%- if tgt_col_list | length > 1 -%} + {%- set _ = tgt_cols_output.update({col: tgt_col_list}) -%} + {%- else -%} + {%- set _ = tgt_cols_output.update({col: tgt_col_list[0]}) -%} + {%- endif -%} + + {%- else -%} + {%- set _ = tgt_cols_output.update({col: tgt_col}) -%} + {%- endif -%} + +{% endfor %} + +{{ return(tgt_cols_output) }} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/get_col_list.sql b/macros/internal/get_col_list.sql new file mode 100644 index 000000000..06f744ef5 --- /dev/null +++ b/macros/internal/get_col_list.sql @@ -0,0 +1,48 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro get_col_list(tgt_cols) -%} + + +{%- set col_list = [] -%} + +{%- if tgt_cols is iterable -%} + + {%- for columns in tgt_cols -%} + + {%- if columns is string -%} + + {%- set _ = col_list.append(columns) -%} + + {#- If a triple -#} + {%- elif columns | first is string -%} + + {%- set _ = col_list.append(columns|last) -%} + + {#- If list of lists -#} + {%- elif columns is iterable and columns is not string -%} + + {%- for cols in columns -%} + + {%- set _ = col_list.append(cols|last) -%} + + {%- endfor -%} + {%- endif -%} + + {%- endfor -%} +{%- endif -%} + +{{ return(col_list) }} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/is_union.sql b/macros/internal/is_union.sql new file mode 100644 index 000000000..088c13cd7 --- /dev/null +++ b/macros/internal/is_union.sql @@ -0,0 +1,50 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro is_union(obj) -%} + +{%- if obj is iterable and obj is not string -%} + {%- set checked_relations = [] -%} + {%- for source in obj -%} + {%- set _ = checked_relations.append(dbtvault.check_relation(source)) -%} + {%- endfor -%} + + {#- Not a union if only one source -#} + {%- if checked_relations | length == 1 -%} + + {{- return(false) -}} + + {%- else -%} + {#- Check all are relations -#} + {%- set test_outcome = checked_relations | unique | list -%} + + {%- if test_outcome | length > 1 -%} + + {{- return(false) -}} + + {%- elif test_outcome[0] is sameas true -%} + + {{- return(true) -}} + + {%- else -%} + + {{- return(false) -}} + + {%- endif -%} + + {%- endif -%} + +{%- endif -%} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/union.sql b/macros/internal/union.sql index 0d6cd4c95..b5afb4e7e 100644 --- a/macros/internal/union.sql +++ b/macros/internal/union.sql @@ -14,7 +14,7 @@ -#} {%- macro union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -%} - SELECT {{ dbtvault.prefix([src_pk[0], src_nk[0], src_ldts, src_source], 'src')}}{% if is_incremental() or union -%}, + SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'src')}}{% if is_incremental() or union -%}, LAG({{ src_source }}, 1) OVER(PARTITION by {{ tgt_pk | last }} ORDER BY {{ tgt_pk | last }}) AS FIRST_SOURCE @@ -27,7 +27,7 @@ {%- for src in range(iterations) -%} {%- set letter = letters[loop.index0] %} - {{ dbtvault.single(src_pk[loop.index0], src_nk[loop.index0], src_ldts, src_source, + {{ dbtvault.single(src_pk, src_nk, src_ldts, src_source, source[loop.index0], letter) -}} {% if not loop.last %} UNION diff --git a/macros/tables/hub_template.sql b/macros/tables/hub_template.sql index 1ec136f8f..ab55ddf7f 100644 --- a/macros/tables/hub_template.sql +++ b/macros/tables/hub_template.sql @@ -15,8 +15,18 @@ {%- macro hub_template(src_pk, src_nk, src_ldts, src_source, tgt_pk, tgt_nk, tgt_ldts, tgt_source, source) -%} + +{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_nk=src_nk, src_ldts=src_ldts, src_source=src_source, + tgt_pk=tgt_pk, tgt_nk=tgt_nk, tgt_ldts=tgt_ldts, tgt_source=tgt_source, + source=source) -%} + +{%- set tgt_pk = tgt_cols['tgt_pk'] -%} +{%- set tgt_nk = tgt_cols['tgt_nk'] -%} +{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} +{%- set tgt_source = tgt_cols['tgt_source'] -%} + +{%- set is_union = dbtvault.is_union(source) -%} -- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault -{% set is_union = true if source|length > 1 else false %} SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_nk, tgt_ldts, tgt_source], 'stg') }} FROM ( {{ dbtvault.create_source(src_pk, src_nk, src_ldts, src_source, diff --git a/macros/tables/link_template.sql b/macros/tables/link_template.sql index 33f81b955..c38b8c830 100644 --- a/macros/tables/link_template.sql +++ b/macros/tables/link_template.sql @@ -15,8 +15,18 @@ {%- macro link_template(src_pk, src_fk, src_ldts, src_source, tgt_pk, tgt_fk, tgt_ldts, tgt_source, source) -%} + +{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_fk=src_fk, src_ldts=src_ldts, src_source=src_source, + tgt_pk=tgt_pk, tgt_fk=tgt_fk, tgt_ldts=tgt_ldts, tgt_source=tgt_source, + source=source) -%} + +{%- set tgt_pk = tgt_cols['tgt_pk'] -%} +{%- set tgt_fk = tgt_cols['tgt_fk'] -%} +{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} +{%- set tgt_source = tgt_cols['tgt_source'] -%} + +{%- set is_union = dbtvault.is_union(source) -%} -- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault -{% set is_union = true if source|length > 1 else false %} SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_ldts, tgt_source], 'stg') }} FROM ( {{ dbtvault.create_source(src_pk, src_fk, src_ldts, src_source, diff --git a/macros/tables/sat_template.sql b/macros/tables/sat_template.sql index c3493c261..746663d44 100644 --- a/macros/tables/sat_template.sql +++ b/macros/tables/sat_template.sql @@ -17,23 +17,38 @@ tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source, source) -%} --- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault -{%- set tgt_cols = dbtvault.get_tgt_cols([tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source]) %} -SELECT DISTINCT {{ dbtvault.cast([tgt_hashdiff, tgt_pk, tgt_payload, tgt_ldts, tgt_eff, tgt_source], 'e') }} +{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, + src_hashdiff=src_hashdiff, src_payload=src_payload, src_eff=src_eff, + src_ldts=src_ldts, src_source=src_source, + tgt_pk=tgt_pk, + tgt_hashdiff=tgt_hashdiff, tgt_payload=tgt_payload, tgt_eff=tgt_eff, + tgt_ldts=tgt_ldts, tgt_source=tgt_source, + source=source) -%} + +{%- set tgt_pk = tgt_cols['tgt_pk'] -%} +{%- set tgt_hashdiff = tgt_cols['tgt_hashdiff'] -%} +{%- set tgt_payload = tgt_cols['tgt_payload'] -%} +{%- set tgt_eff = tgt_cols['tgt_eff'] -%} +{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} +{%- set tgt_source = tgt_cols['tgt_source'] -%} + +{%- set tgt_cols_list = dbtvault.get_col_list([tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source]) -%} + +-- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault +SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_hashdiff, tgt_payload, tgt_ldts, tgt_eff, tgt_source], 'e') }} FROM {{ source[0] }} AS e {% if is_incremental() -%} LEFT JOIN ( - SELECT {{ dbtvault.prefix(tgt_cols, 'd') }} + SELECT {{ dbtvault.prefix(tgt_cols_list, 'd') }} FROM ( - SELECT {{ dbtvault.prefix(tgt_cols, 'c') }}, + SELECT {{ dbtvault.prefix(tgt_cols_list, 'c') }}, CASE WHEN RANK() OVER (PARTITION BY {{ dbtvault.prefix([tgt_pk|last], 'c') }} ORDER BY {{ dbtvault.prefix([tgt_ldts|last], 'c') }} DESC) = 1 THEN 'Y' ELSE 'N' END CURR_FLG FROM ( - SELECT {{ dbtvault.prefix(tgt_cols, 'a') }} + SELECT {{ dbtvault.prefix(tgt_cols_list, 'a') }} FROM {{ this }} as a JOIN {{ source[0] }} as b ON {{ dbtvault.prefix([tgt_pk|last], 'a') }} = {{ dbtvault.prefix([src_pk], 'b') }} diff --git a/mkdocs.yml b/mkdocs.yml index 86a795d42..438f14e33 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ repo_url: 'https://github.com/Datavault-UK/dbtvault' nav: - Home: 'index.md' - Getting Started: 'gettingstarted.md' + - Best Practices: 'bestpractices.md' - Loading the vault: - Staging: 'staging.md' - Hubs: 'hubs.md' From ffbe4c968b0eed93ccd1da60dae3e252089e9ab2 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 25 Oct 2019 13:34:07 +0100 Subject: [PATCH 079/157] Version 0.3.1 Added handling for an compilation error which occurs when an incorrect source mapping is provided for a model and a source relation is provided for a target mapping, causing missing columns in generated SQL. --- macros/internal/create_tgt_cols.sql | 2 ++ macros/internal/validate_columns.sql | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 macros/internal/validate_columns.sql diff --git a/macros/internal/create_tgt_cols.sql b/macros/internal/create_tgt_cols.sql index d23e4d1ef..1136015b6 100644 --- a/macros/internal/create_tgt_cols.sql +++ b/macros/internal/create_tgt_cols.sql @@ -59,6 +59,8 @@ {%- set columns = adapter.get_columns_in_relation(source[0]) -%} {%- set column_names = columns | map(attribute='name') | list -%} +{{ dbtvault.validate_columns(src_cols_list, column_names, source[0]) }} + {%- for col in tgt_cols_dict -%} {%- set src_cols = tgt_cols_dict[col][0] -%} diff --git a/macros/internal/validate_columns.sql b/macros/internal/validate_columns.sql new file mode 100644 index 000000000..b01282539 --- /dev/null +++ b/macros/internal/validate_columns.sql @@ -0,0 +1,25 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro validate_columns(select_columns, source_columns, source_relation) -%} + +{%- if source_columns -%} + {%- for col in select_columns -%} + {%- if col not in source_columns -%} + {{ exceptions.raise_compiler_error("Column '" ~ col ~ "' not present in source '" ~ source_relation.table ~ "', either incorrect source or incorrect source column name.") }} + {%- endif -%} + {%- endfor -%} +{%- endif -%} + +{%- endmacro -%} \ No newline at end of file From bb885e101dc0197639ecab51d393e5d9f82adae1 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 25 Oct 2019 13:43:28 +0100 Subject: [PATCH 080/157] 0.3.1 docs update Changed 0.3 references to 0.3.1 --- README.md | 4 ++-- dbt_project.yml | 2 +- docs/changelog.md | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fba39d9d9..5cd4ef947 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3-pre)](https://dbtvault.readthedocs.io/en/v0.3-pre/?badge=v0.3-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.1-pre)](https://dbtvault.readthedocs.io/en/v0.3.1-pre/?badge=v0.3.1-pre) [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) @@ -34,7 +34,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.3-pre # Latest stable version + revision: v0.3.1-pre # Latest stable version ``` And run ```dbt deps``` diff --git a/dbt_project.yml b/dbt_project.yml index 70b74be93..0dd7b131b 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,5 +1,5 @@ name: 'dbtvault' -version: '0.3' +version: '0.3.1' profile: 'dbtvault' diff --git a/docs/changelog.md b/docs/changelog.md index 5ffb854ef..d20419dfe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.3.1-pre] - 2019-10-25 +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.1-pre)](https://dbtvault.readthedocs.io/en/v0.3.1-pre/?badge=v0.3.1-pre) + +### Error handling + +- An exception is now raised with an informative message when an incorrect source mapping is +provided for a model in the case where a source relation is also provided for a target mapping. +This caused missing columns in generated SQL, and a misleading error message from dbt. + ## [v0.3-pre] - 2019-10-24 [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3-pre)](https://dbtvault.readthedocs.io/en/v0.3-pre/?badge=v0.3-pre) From 5c066e8f7777f83950193693d6ac435c2a9a5524 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 28 Oct 2019 15:46:55 +0000 Subject: [PATCH 081/157] Version 0.3.2 Bug Fixes - Fixed a bug where the logic for performing a base-load (loading for the first time) on a union-based hub or link was incorrect, causing a load failure. Documentation - Various corrections and clarifications on the macros page. --- README.md | 4 +-- dbt_project.yml | 2 +- docs/bestpractices.md | 45 ++++++++++++++++++++++--- docs/changelog.md | 11 +++++++ docs/macros.md | 76 ++++++++++++++++++++++++++++++------------- 5 files changed, 108 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 5cd4ef947..e77bd65ab 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.1-pre)](https://dbtvault.readthedocs.io/en/v0.3.1-pre/?badge=v0.3.1-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.2-pre)](https://dbtvault.readthedocs.io/en/v0.3.2-pre/?badge=v0.3.2-pre) [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) @@ -34,7 +34,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.3.1-pre # Latest stable version + revision: v0.3.2-pre # Latest stable version ``` And run ```dbt deps``` diff --git a/dbt_project.yml b/dbt_project.yml index 0dd7b131b..50cf18969 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,5 +1,5 @@ name: 'dbtvault' -version: '0.3.1' +version: '0.3.2' profile: 'dbtvault' diff --git a/docs/bestpractices.md b/docs/bestpractices.md index 6532b0e40..de6d8e29f 100644 --- a/docs/bestpractices.md +++ b/docs/bestpractices.md @@ -31,16 +31,53 @@ If there is already a source in the raw staging layer, you may keep this or over ## Hashing -Best practises for hashing include: +!!! seealso "See Also" + - [hash](#hash) + - [multi-hash](macros.md#multi_hash) + +### The drawbacks of using MD5 -- Alpha sorting hashdiff columns. dbtvault does this for us, so no worries! Refer to the [multi-hash](macros.md#multi_hash) docs for how to do this +We are using md5 to calculate the hash in the macros above. If your table contains more than a few billion rows, +then there is a chance of a clash: where two different values generate the same hash value +(see [Collision vulnerabilities](https://en.wikipedia.org/wiki/MD5#Collision_vulnerabilities)). + +For this reason, it **should not be** used for cryptographic purposes either. + +In future releases of dbtvault, we will allow you to change the algorithm that is used (e.g. to SHA-256) to reduce the +chance of a clash (at the expense of more processing and a larger column), or switch off hashing entirely. + +### Why do we hash? + +Data Vault uses hashing for two different purposes. + +#### Primary Key Hashing + +A hash of the primary key. This creates a surrogate key, but it is calculated consistently across the database: +as it is a single column, same data type, it supports the use of pattern-based loading. + +#### Hashdiffs + +Used to finger-print the payload of a satellite (similar to a checksum) so it is easier to detect if there has been a +change in payload, to trigger the load of a new satellite record. This simplifies the SQL as otherwise we'd have to +compare each column in turn and handle nulls to see if a change had occured. + +Hashing is sensitive to column ordering. You can ask the macro to sort the columns alphabetically for you +(as per best practices), or switch this off and let your order take precedence (by setting the sort parameter +to true or false accordingly). Columns are sorted by their alias. + +### Hashing best practices + +Best practices for hashing include: + +- Alpha sorting hashdiff columns. As mentioned, dbtvault can do this for us, so no worries! +Refer to the [multi-hash](macros.md#multi_hash) docs for details on how to do this. - Ensure all **hub** columns used to calculate a primary key hash are presented in the same order across all staging tables !!! note - Some tables may use different column names for primary key components, so we cannot sort the columns for - you as we do with hashdiffs. + Some tables may use different column names for primary key components, so you generally **should not** use + the sorting functionality for primary keys. - For **links**, columns must be sorted by the primary key of the hub and arranged alphabetically by the hub name. The order must also be the same as each hub. \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index d20419dfe..1e49ef888 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.3.2-pre] - 2019-10-28 +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.2-pre)](https://dbtvault.readthedocs.io/en/v0.3.2-pre/?badge=v0.3.2-pre) + +### Bug Fixes + +- Fixed a bug where the logic for performing a base-load (loading for the first time) on a union-based hub or link was incorrect, causing a load failure. + +### Documentation + +- Various corrections and clarifications on the macros page. + ## [v0.3.1-pre] - 2019-10-25 [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.1-pre)](https://dbtvault.readthedocs.io/en/v0.3.1-pre/?badge=v0.3.1-pre) diff --git a/docs/macros.md b/docs/macros.md index 4da871348..3f7e2a79d 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -1,26 +1,44 @@ ## Table templates ######(macros/tables) -These macros form the core of the package and can be called in your models to build the tables for your Data Vault. +These macros form the core of the package and can be called in your models to build the different types of tables needed +for your Data Vault. +### Metadata notes #### Using a source reference for the target metadata -As of release 0.3, you may now use a reference as a target metadata value, to streamline metadata entry. +!!! note + As of release 0.3, you may now use a source reference as a target metadata value, to streamline metadata entry. + Read below! -In the usage examples for the table template macros in this section, you will see ```source``` provided as the values for some -of the target metadata variables. ```source``` has been declared as a variable at the top of the models, and holds a -reference to the source table we are loading from. This is shorthand for retaining the name and data types of the columns as they were -provided in the ```src``` variables. You may wish to alias the columns or change their data types in specific -circumstances, which is possible by providing a triple in the form of a list. +In the usage examples for the table template macros in this section, you will see ```source``` provided as the values +for some of the target metadata variables. ```source``` has been declared as a variable at the top of the models, +and holds a reference to the source table we are loading from. This is shorthand for retaining the name and data types +of the columns as they are provided in the ```src``` variables. You may wish to alias the columns or change their data +types in specific circumstances, which is possible by providing an additional parameter as a list of triples: +``` (source column name, data type to cast to, target column name)```. Both approaches are shown in the snippet below: ```mysql +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- ...other src metadata... -%} + {%- set tgt_pk = source -%} {%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} ``` +Here, we are keeping the ```tgt_pk``` (the target table's primary key) the same as the primary key identified in the +source (```src_pk```). +Behind the scenes, the macro will get the datatype of the column provided in the ```src_pk``` variable and generate a +mapping for us. If the ```src_pk``` column does not exist, an appropriate exception will be raised. + +Alternatively we have provided a manual mapping for the ```tgt_fk``` (the target table's foreign key). + +*For further details and examples on both methods, refer to the usage examples +and snippets in the table template documentation below (both Single-Source and Union).* !!! note If only aliasing and **not** changing data types, we suggest using the [add_columns](#add_columns) macro. @@ -30,7 +48,7 @@ ___ ### hub_template -Creates a hub with provided metadata. +Generates sql to build a hub table using the provided metadata. ```mysql dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, @@ -152,7 +170,7 @@ ___ ### link_template -Creates a link with provided metadata. +Generates sql to build a link table using the provided metadata. ```mysql dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, @@ -228,7 +246,6 @@ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, source) }} ``` - #### Output ```mysql tab="Single-Source" @@ -280,7 +297,7 @@ ___ ### sat_template -Creates a satellite with provided metadata. +Generates sql to build a satellite table using the provided metadata. ```mysql dbtvault.sat_template(src_pk, src_hashdiff, src_payload, @@ -398,9 +415,10 @@ ___ [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) !!! seealso "See Also" - [hash](#hash) + - [hash](#hash) + - [Hashing best practises and why we hash](bestpractices.md#hashing) -A macro for generating multiple lines of hashing SQL for columns: +This macro will generate SQL hashing sequences for one or more columns as below: ```sql CAST(MD5_BINARY(UPPER(TRIM(CAST(column1 AS VARCHAR)))) AS BINARY(16)) AS alias1, CAST(MD5_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(16)) AS alias2 @@ -437,9 +455,9 @@ CAST(MD5_BINARY(CONCAT( ``` !!! success "Column sorting" - You do not need to worry about providing the columns in any particular order, as long as you set the - ```sort``` flag to true when creating hashdiffs. - + If you wish to sort columns in alphabetical order as per [best practices](bestpractices.md#hashing), + you do not need to worry about doing this manually, just set the + ```sort``` flag to true when creating hashdiffs as per the above example. ___ ### add_columns @@ -483,16 +501,21 @@ OLD_CUSTOMER_PK AS CUSTOMER_PK The ```add_columns``` macro will automatically select all columns from the optional ```source_table``` reference, if provided. -##### Overring source columns +##### Overriding source columns You may wish to override some of the source columns with different values. To replace the ```SOURCE``` or ```LOADDATE``` column value, for example, then you must provide the column name that you wish to override as the alias in the pair. +!!! note + The macro will not actually override (delete or replace) any of the source columns, but simply add new columns + using the provided column as a basis. + ##### Functions -Database functions may be used, for example ```CURRENT_DATE()```, to set the current date as the value of a column, as on -```line 2``` of the usage example. +Database functions may be used, for example ```CURRENT_DATE()``` to set the current date as the value of a column, as on +```line 2``` of the usage example. Any function supported by the database is valid, for example ```LPAD()```, which pads +a column with leading zeroes. ##### Adding constants With the ```add_columns``` macro, you may provide constants. @@ -502,9 +525,11 @@ and the macro will do the rest. See ```line 3``` of the usage example above, and ##### Aliasing columns -As of release 0.3, columns must now be aliased prior to loading, in the staging layer. This can be done by providing the +As of release 0.3, columns should now be aliased in the staging layer prior to loading. This can be achieved by providing the column name you wish to alias as the first argument in a pair, and providing the alias for that column as the second argument. -This process can be observed on ```line 4``` of the usage example above. +This can be observed on ```line 4``` of the usage example above. Aliasing can still be carried out using a +manual mapping (shown in the [table template](#table-templates) section examples) but this is less concise for aliasing +purposes. ___ @@ -517,7 +542,7 @@ FROM MYDATABASE.MYSCHEMA.MYTABLE ``` !!! info - Sources need to be set up in dbt. [Read More](https://docs.getdbt.com/docs/using-sources) + Sources need to be set up in dbt to ensure this works. [Read More](https://docs.getdbt.com/docs/using-sources) #### Parameters @@ -604,6 +629,10 @@ ___ The intended use is for creating checksum-like fields only, so that a record change can be detected. [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) + +!!! seealso "See Also" + - [multi-hash](#multi_hash) + - [Hashing best practises and why we hash](bestpractices.md#hashing) A macro for generating hashing SQL for columns: ```sql @@ -611,7 +640,8 @@ CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias ``` - Can provide multiple columns as a list to create a concatenated hash -- Hashdiffs should be alpha sorted using the ```sort``` flag. +- Columns are sorted alphabetically (by alias) if you set the ```sort``` flag to true. +- Generally, you should alpha sort hashdiffs using the ```sort``` flag. - Casts a column as ```VARCHAR```, transforms to ```UPPER``` case and trims whitespace - ```'^^'``` Accounts for null values with a double caret - ```'||'``` Concatenates with a double pipe From 06e404f5368503efcbeed01ad7ed899be26d166f Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 28 Oct 2019 15:53:01 +0000 Subject: [PATCH 082/157] Adding the macro changes for 0.3.2 - Fixed a bug where the logic for performing a base-load (loading for the first time) on a union-based hub or link was incorrect, causing a load failure. - Deleted deprecated internal macro --- macros/internal/get_tgt_cols.sql | 44 -------------------------------- macros/internal/union.sql | 3 +-- macros/tables/hub_template.sql | 10 ++++++-- macros/tables/link_template.sql | 10 ++++++-- 4 files changed, 17 insertions(+), 50 deletions(-) delete mode 100644 macros/internal/get_tgt_cols.sql diff --git a/macros/internal/get_tgt_cols.sql b/macros/internal/get_tgt_cols.sql deleted file mode 100644 index 11cf8ce34..000000000 --- a/macros/internal/get_tgt_cols.sql +++ /dev/null @@ -1,44 +0,0 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} -{%- macro get_tgt_cols(tgt_cols) -%} - -{%- set col_list = [] -%} - -{%- if tgt_cols is iterable -%} - - {%- for col_set in tgt_cols -%} - - {#- If a triple -#} - {%- if col_set | first is string -%} - - {%- set _ = col_list.append(col_set|last) -%} - - {#- If list of lists -#} - {%- elif col_set is iterable and col_set is not string -%} - - {%- for cols in col_set -%} - - {%- set _ = col_list.append(cols|last) -%} - - {%- endfor -%} - - {%- endif -%} - - {%- endfor -%} -{%- endif -%} - -{{ return(col_list) }} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/union.sql b/macros/internal/union.sql index b5afb4e7e..a474c182b 100644 --- a/macros/internal/union.sql +++ b/macros/internal/union.sql @@ -14,11 +14,10 @@ -#} {%- macro union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -%} - SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'src')}}{% if is_incremental() or union -%}, + SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'src')}}, LAG({{ src_source }}, 1) OVER(PARTITION by {{ tgt_pk | last }} ORDER BY {{ tgt_pk | last }}) AS FIRST_SOURCE - {%- endif %} FROM ( {%- set letters='abcdefghijklmnopqrstuvwxyz' -%} diff --git a/macros/tables/hub_template.sql b/macros/tables/hub_template.sql index ab55ddf7f..0a7cbe518 100644 --- a/macros/tables/hub_template.sql +++ b/macros/tables/hub_template.sql @@ -33,12 +33,18 @@ FROM ( tgt_pk, tgt_nk, tgt_ldts, tgt_source, source, is_union) }} ) AS stg -{% if is_incremental() or is_union -%} +{# If incremental union or single #} +{%- if is_incremental() -%} LEFT JOIN {{ this }} AS tgt ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL -{%- if is_union %} +{# If an incremental and union load -#} +{% if is_union -%} AND stg.FIRST_SOURCE IS NULL {%- endif -%} {%- endif -%} +{# If a union base-load #} +{%- if is_union and not is_incremental() -%} +WHERE stg.FIRST_SOURCE IS NULL +{%- endif -%} {%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/link_template.sql b/macros/tables/link_template.sql index c38b8c830..c02dc62e5 100644 --- a/macros/tables/link_template.sql +++ b/macros/tables/link_template.sql @@ -33,12 +33,18 @@ FROM ( tgt_pk, tgt_fk, tgt_ldts, tgt_source, source, is_union) }} ) AS stg -{% if is_incremental() or is_union -%} +{# If incremental union or single #} +{%- if is_incremental() -%} LEFT JOIN {{ this }} AS tgt ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL -{%- if is_union %} +{# If an incremental and union load -#} +{% if is_union -%} AND stg.FIRST_SOURCE IS NULL {%- endif -%} {%- endif -%} +{# If a union base-load #} +{%- if is_union and not is_incremental() -%} +WHERE stg.FIRST_SOURCE IS NULL +{%- endif -%} {%- endmacro -%} \ No newline at end of file From e00969624ca9cc0c9bbe12cda4a3897174458a0c Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 31 Oct 2019 15:04:07 +0000 Subject: [PATCH 083/157] Added full snowflake worked example --- docs/assets/images/database.png | Bin 0 -> 9717 bytes docs/assets/images/tpch.png | Bin 0 -> 286528 bytes docs/assets/images/warehouse.png | Bin 0 -> 20919 bytes docs/bestpractices.md | 4 +- docs/contributing.md | 14 +-- docs/demonstration.md | 6 -- docs/gettingstarted.md | 71 --------------- docs/hubs.md | 2 +- docs/index.md | 1 - docs/loading.md | 148 ++++++++++++++++++++++++++++++ docs/macros.md | 4 +- docs/setup.md | 123 +++++++++++++++++++++++++ docs/sourceprofile.md | 75 +++++++++++++++ docs/staging.md | 4 +- docs/stagingdemo.md | 151 +++++++++++++++++++++++++++++++ docs/stylesheets/cube.css | 12 --- docs/stylesheets/extra.css | 37 ++++++++ docs/workedexample.md | 40 ++++++++ mkdocs.yml | 19 +++- 19 files changed, 602 insertions(+), 109 deletions(-) create mode 100755 docs/assets/images/database.png create mode 100755 docs/assets/images/tpch.png create mode 100755 docs/assets/images/warehouse.png delete mode 100644 docs/demonstration.md delete mode 100644 docs/gettingstarted.md create mode 100644 docs/loading.md create mode 100644 docs/setup.md create mode 100644 docs/sourceprofile.md create mode 100644 docs/stagingdemo.md delete mode 100644 docs/stylesheets/cube.css create mode 100644 docs/stylesheets/extra.css create mode 100644 docs/workedexample.md diff --git a/docs/assets/images/database.png b/docs/assets/images/database.png new file mode 100755 index 0000000000000000000000000000000000000000..a3e279b176732d58ffac40b15381f32d99609da1 GIT binary patch literal 9717 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

!8SpWd^C_a_gSRWS@V0m z;!MEASmP9jO`a4P1(~dEHCAXh9`~Y{rc6jx?o05zM5!96jAe>f3Vyp&s|R7$=(vT< zF!fFuBUCB!FY6QY#GUBS6fq!-m#ylCbKYo}TOj71apQL+WxaUnTC238ym` zPXGc5M5ak^3Af2>d0S+}zX$(xmq3RZRvjois>`ojmPNay&=70&<63!hSHbmx&b56& z)AU!NY;HY;!6#IH;gJ4Ki72z#!ULqacPGw5(mq`Mmoc;RQzaw)wmI`pPo^E$*^4Uc z_mXuW*Ye55gYdN&P%~f?cd07o$>v5?NdAKlFKl8g{3f^=E9EkvMN*HEcw)FPz1F4&Q5~t5LRd&@Nc4C05mdu;dOh3a>4SU- zV#URz>|_}w>i%CUY=T*%6KsJP(s_<^R4MiODX_?}R=5wof3QRo+ zCuuEgwC%a#UHkEyt{d`;dv%`4)P~&47We*0_%vi3EKdl$et2t0r7z+Yr*C+vSqK-==6ezI>yxf2QaCVZp0H$-Cwy*T~32j z>lb!~W~NP__R8K7pGXiRguw5~xO?S|W`DKIZWsw!T2f`95Q{FZtH(F&7TO3s=@?U# zzL%eVT2bGIwDX!LQ9Af^&EEuKd}?b~ZDZ8;%3HeE@l*IMP2=lguMd_XObRm2x{E21#svF%`}(34TuWlJH-VSV z<^aX+bkfuQ`q;h&946=rmSD&uU2n!}5iHt!(c0!gKz9EZ-xOFteAKX}9}O?IATLgWZ7~lpstm#VlbUHC&rNCG2y|Du8b`@;98|a7>Z%Ea_-0egV^YrXWvUVlTh*Fr}<9#INq7Qm%qNE$3J9*{f zZICt@^!1VCVBjm|lOq$9nLl!mmFc&df$Zl~;|{AC^bYe9WzYAf=_l|%%L$KhawW~1 z@#+(p*<*Xci=grjX+3wDwCq;uE9M>g5#)BaVrC7=nlc?y$a=;cb87ctx0h{K;4K;+ zh6m?D&VYbchc`TsRx;1!QrcVB`dP2GjwkxjV#(K>t|9hND45CE2oZ%!{5E8_iStS< zrRleCvS#nq0~mqiHP4@4re$*vn=+3$F x>&?1>D6Q@2OUflElyEheb8bz4Dgm~*z3TyBG` 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

+`9CPEi&(EF^@aX(72EV9&N zAJRt3Qoo=Qx{MbFI|V)jos-tBhLc*`b!>fdm+@KvTK z#4r5_V_W{Pbe8As|0Rr}rWdPx?6_z5m*m&{TGcE?8C5ksB3V^`RQ>jjkuNo0U0%iJ zzkFB-go*q*1-03jg?($|1g~$p?Ox*fQ=#u#>O9u0UKM1x;6(Z&P&kUB0Dj&oCethx zpE9obPpg~tB1vJNB6l^{HIC0u$9T78O5vt$<8K_O@4gzL*xazT={e5tYeAg!S0n;z z=?0l-dTo>Lp`J!4Q_h4By1N`f9$yZNb7f#hM&&5xn&7Cla4!6Qz97+#)mu$vd@D&+ zE4Or;-UQa0$EmY^{4}tfhj8s}I>GXBsPO-nM;UcQ-O&~EWVBCA`XRDe2w_^RAP%wTji#D%gp}x zxPdm0Z2lhUDf+%Aa@cH*lqPmI&iQgvOE;MjL@%uN(4(5qqiI7lVnVnf>Gcrle-8FK ze|(-5dZ5?TC0R^51hyNrFC=wx19tem(m@ATW40d^1w}{!8b?LlHtmIzGhWpa$~JW8 zwJZ9zSot<2__Y5yLuyF=<8e=MVSSmjcIlds>8hI&I42~vnxdgujLWUGWah3XPB`m( zWm4D9Rnf-qT}Mz=24-nXt;Ah>tr>)gfmt}7cRe3Z$31Uj7LUv9c-PuBA8_$cO^kGJxAG|ur=1GbaXNr0 z5=T4PGmes_&z) zVzGN7=!^dBIH7GMm0)wBu3glrC~q}u$)U6WPj^tuw?Vt-R3bjKLQ;FYkM335FU%Qw zCbZMdMZG$6#DqTGje9%`j1;`h_-Yj}tMb+^GPR@W(Hl>832)TNC)FzBiv$%uyIQeV z5&da#D3GT1zWzDH%zk$mKb7Bo^1-!<(1!sWyRz1qbY)giIx?u(o2!I+BDzwUAejF# z!vgDC&$F6x0(JD3Kk{PIvNxvb-jrxfsKd1%&fc#0;L6oebTUx1Yl>1RoYO4o>whi2 zXF`1;-b=0CuHj!uKxQ=T$h$dw?A;9LymHKquP_=;kgVVEpOasasU>?Zbr}}-+qr!bFnur0s1}h^=Ly~E2THhpMF)nJqqWdK$ej$! zE=JVz_l**Rq6-Kj-}8jMKZ&Wj{9cFrRCB8Ta`35L;j4Am;1d5Vk|NLE(La&-Ar7#V%{*Qj~aQs2Lw?VWSizTz9Oto`JFs<$_y&K8N@#pb+6HyU2__PE4{=sNsT zccwzZ=JN>66${z(WoA(VClBF9Nl9+HNi{@`ayuA~JhuQ*Amj!D6Qvd2T2xt)8_xheZf=ymD@@UJXqE8%=;*oCsBA2MYBdszH-rqn`c;qld@wHlMDWkhX8Cv`0Ju)>2lB zv9rHEbt@SNc4XS)K2lN#xPG+8u7e}%%J#fn{mXRaKq*I-{#zqPl* zZ=hiR+}s0DT8z{cU!~YcVNE}b^YO-%`NS+t#sG+Oz~iS0`hD02EgxhWYe&qqA+tkwIfQ#Adv!y2FeK6FA>Yp6 z6vtrguIR0>MC62?yXuxZhu!xPr=5PzNr#%@vjD>?pt+qo_>tiNwCP~tKHxE$4%U;x zVPqKE{4CZNzVS6}j5+=Ka)&R`_DJd^KWEZ|9>}DP%9ZCo2Xw7r3ZJt%IFco^ziBFA zjAZ8)H*-S2aRv_(1?*DiZMEiaJ*QZ)>1O;^T=xLC6z=)|=DS6Zs=BM9E4HE=t;QOG ztY;^}yz9n<(eDl~CCALTBoB!gPw%-+Ux{jNP`$_*Lu5~N`B;4R3cV5N89-A_VMo@(6lOdPeLl^L;e{mkf7-xLX6^02~f!Xg$VoZoT|$~&Hd?SWFR<@ zL5PRIGoS3->xd3~{xX)GxH=GfCYaoXj~F7%?PsQQ*VFJ?yieJ4xqnsr9YKZQf16eK zjUNy8`pz52c?(s1zgW2C;9|ZrP433b8=Lt{tbj^KWN1<^aO`g;g6FHprOU0g?nD#0 z4fY@6WKq6gDpfX%ybMKZo)}ZVVVWO*6_fcDx|`$s)f&3Cukbb;e7V8rDP^~dOW|Qx z76ol%+&ZsqKQrhG8duWVos}V4>6JDrW!wIGS9LRGECm7^zwUe;hn)m)(1!ohO?N)l zUV^#ogvUR-A*8%lR2|AASF^wxYJX_rg{H)}&%tCA898yu?L#0sAo!wUN2=@cNDlVa z$CHLj(4Btf6swVkmV!5S(SGuZe%$@`%ey|VDy0IM+f(^9w#`w;W(&f<4M=%!LfHDC zp!b9g$B*_C>Gu&XJQpYD6kQ{GaZSFm+vN@YU^qh>+NFns7r@umB(rB368yQ@c++rn z(9XhV!5H!Z!AA10sQH=jCZyW@hO1QKucUv46e3N>5EQhi66(Ji92qPpEC+?(vxBL{w=QmDo!>fWS{o4ycOnON&h+w8aav^ z1yO?KuU7b1sr<~5qPRoV0aM!lr=7l#YW&};`1&UP%A<kmAYLYF=>I<| C8DqHs literal 0 HcmV?d00001 diff --git a/docs/assets/images/warehouse.png b/docs/assets/images/warehouse.png new file mode 100755 index 0000000000000000000000000000000000000000..f6a0262d211c731d4184b62ba5395acaadbb9c7a GIT binary patch literal 20919 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 + Find out more + \ No newline at end of file diff --git a/docs/macros.md b/docs/macros.md index 3f7e2a79d..026513f39 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -508,8 +508,8 @@ or ```LOADDATE``` column value, for example, then you must provide the column na that you wish to override as the alias in the pair. !!! note - The macro will not actually override (delete or replace) any of the source columns, but simply add new columns - using the provided column as a basis. + If a provided column name is the same as a source column name, the provided + column will take precedence over the source column, and the original source column will not be selected. ##### Functions diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 000000000..e7b787b62 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,123 @@ +## Download the demonstration project + +Assuming you already have a python environment installed, the next step is to download the latest +demonstration project from the repository. + +Using the button below, find the latest release and download the zip file, listed under assets. + + + View Downloads + + +Once downloaded, unzip the project. + +## Installing requirements + +Once you have downloaded the project, install all of the requirements from the provided ```requirements.txt``` file. +First make sure the ```requirements.txt``` file is in your current working directory, then run: + +```pip install -r requirements.txt``` + +This will install dbt and all of its dependencies, ready for +development with dbt. + +## Install dbtvault + +Next, we need to install dbtvault. +dbtvault has already been added to the ```packages.yml``` file provided with the example project, so all you need to do +is run the following command: + +```dbt deps``` + +## Setting up dbtvault with Snowflake + +In the provided dbt project file (```dbt_project.yml```) the profile is named ```snowflake-demo```. +In your dbt profiles, you must create a connection with this name and provide the snowflake +account details so that dbt can connect to your Snowflake databases. + +dbt provides their own documentation on how to configure profiles, so we suggest reading that +[here](https://docs.getdbt.com/docs/configure-your-profile). + +A sample profile configuration is provided below which will get you started: + +```profiles.yml``` +```yaml +snowflake-demo: + target: dev + outputs: + dev: + type: snowflake + account: + + user: + password: + + role: + database: DV_PROTOTYPE_DB + warehouse: DV_PROTOTYPE_WH + schema: DEMO + threads: 4 + client_session_keep_alive: False +``` + +Replace everything in this configuration marked with```<>``` with your own Snowflake account details. + +Key points: + +- You must also create a ```DV_PROTOTYPE_DB``` database and ```DV_PROTOTYPE_WH``` warehouse. + + + +- Your ```DV_PROTOTYPE_WH``` warehouse should be X-Small in size and have a 5 minute auto-suspend, as we will +not be coming close to the limits of what Snowflake can process. + + + +- The role can be anything as long as it has full rights to the above schema and database, so we suggest the +default ```SYSADMIN```. + +- We have set ```threads``` to 4 here. This setting dictates how +many models are processed in parallel. In our experience, 4 is a reasonable amount and the full system is created in a +reasonable time-frame, however, you may run with as many threads as required. + +![alt text](./assets/images/database.png "Creating a database in snowflake") +![alt text](./assets/images/warehouse.png "Creating a warehouse in snowflake") + +## The project file + +The ```dbt_project.yml``` file provided with the project is mostly standard. The main additions are the +settings for the models and the ```vars```. + +```dbt_project.yml``` +```yaml + +models: + snowflakeDemo: + load: + schema: "VLT" + enabled: true + materialized: incremental + stage: + schema: "STG" + enabled: true + materialized: view + raw: + schema: "RAW" + enabled: true + materialized: incremental + vars: + date: TO_DATE('1992-01-08') +``` + +#### models + +Here we are specifying that models in the ```load``` directory should be loaded in to the ```VLT``` +schema, and models in the sub-directories ```stage``` and ```source``` should have their own schemas, +```STG``` and ```SRC``` respectively. We have also specified that they are all enabled, as well +as their materialization. Many of these attributes are also provided in the files themselves and take +precedence over these settings anyway, this is just a design choice. + +#### vars + +To simulate day-feeds, we use a variable we have named ```date``` which is used in the ```SRC``` models to +load for a specific date. This is described in more detail in the [Profiling TPC-H](sourceprofile.md) section. \ No newline at end of file diff --git a/docs/sourceprofile.md b/docs/sourceprofile.md new file mode 100644 index 000000000..cef483d15 --- /dev/null +++ b/docs/sourceprofile.md @@ -0,0 +1,75 @@ +We are using the [TPC-H benchmarking dataset provided by Snowflake](https://docs.snowflake.net/manuals/user-guide/sample-data-tpch.html) +to demonstrate dbtvault and showcase the Data Vault architecture running on Snowflake. + +The data comes in 4 different sizes, we will be using the smallest in this guide, TPCH_SF10 which +contains 60 million rows in its largest table. + +Our aim is to simulate day-feeds into the Data Vault to demonstrate the loading process in a production +environment. Before we begin, the data needs to be profiled to identify patterns in the data +that could be used to help build the Data Vault and create an accurate simulation. + +The below diagram describes the TPC-H system. + +![alt text](./assets/images/tpch.png "ERD for the TPC-H dataset") +(source: [TPC Benchmark H Standard Specification](http://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v2.17.1.pdf)) + + +### Date fields + +There are a total of four date fields in the data set. + +Three of these are found in the ```LINEITEM``` table: + +- ```SHIPDATE``` +- ```COMMITDATE``` +- ```RECEIPTDATE``` + +And one in the ```ORDERS``` table: + +- ```ORDERDATE``` + +Through querying the data, we discovered that the dates behave as expected and appear in chronological order +the majority of the time: ```ORDERDATE```, ```SHIPDATE```, ```RECEIPTDATE```, ```COMMITDATE```, with ```COMMITDATE``` +occasionally going against this pattern. + +This pattern allows us to simulate a system feed over multiple days, but we need to know the range of dates +for the simulation to be accurate. We queried the data to find the maximum and minimum ```ORDERDATE``` and work out the +difference between them. We found the dates spanned around 6.59 years, or 2405 days. + +### Relationships + +Working out relationships between tables and fields is a key step in mapping an existing system to Data Vault, +as it ensures an accurate model of the system is built. + +#### Orders and Suppliers + +We first looked at the relationship between orders and suppliers by doing inner joins on +the ```LINEITEM``` and ```ORDERS``` table, with the ```SUPPLIER``` table and counting the distinct suppliers for each order. +We discovered that it is a one to many relationship: an order can contain parts which are from different suppliers. + +#### Customers and Orders + +Next we looked at the relationship between customers and orders. We wanted to check whether any customers exist without orders. +We did this by doing a left outer join on the ```ORDERS``` table, with the ```CUSTOMER``` table and discovered that several +customers exist without orders. + +### Conclusions + +To create a source feed simulation with the static data (shown by the logical pattern in the date fields), we can use +the ```ORDERDATE``` as a reference date. We can simulate historical data by only loading records before a particular +```ORDERDATE```. Any records in the history where the ```SHIPDATE```, ```RECEIPTDATE``` and ```COMMITDATE``` are after +reference ```ORDERDATE``` will be included but set to ```NULL``` to allow us to simulate existing records being updated +in a new day-feed. + +By profiling the relationships we have identified that the ```PARTSUPP``` table can more appropriately be referred to as +```INVENTORY```, since it is a static relationship (there is no date involved and therefore no changes). This means that +data involving the ```PARTSUPP```, ```SUPPLIER``` and ```PARTS``` tables create an inventory which can be linked +to the ```LINEITEM``` table. + +The relationship between customers and orders tells us that customers without an order will not be loaded into the Data +Vault, as we are using the ```ORDERDATE``` for day-feed simulation. + +Now that we have profiled the data, we cna make more informed decisions when mapping the source system to the Data Vault +architecture. + + diff --git a/docs/staging.md b/docs/staging.md index a4dd66c27..1dc18b445 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -23,7 +23,7 @@ We also need to ensure column names align with target hub or link tables. ## Creating the model -To prepare our raw staging layer for loading the vault, we can create a dbt model and call dbtvault staging macros with +To prepare our raw staging layer for loading the vault, we create a dbt model and call dbtvault staging macros with provided metadata. Our model will consist of: @@ -74,7 +74,7 @@ in our model. {%- set source_table = source('MYSOURCE', 'stg_customer') -%} ``` -### Adding the metadata +### Generating hashes from metadata Now we get into the core component of staging: the metadata. The metadata consists of the column names we want to use in our hash, to use as primary keys in our data vault or to use as diff --git a/docs/stagingdemo.md b/docs/stagingdemo.md new file mode 100644 index 000000000..f58170f2c --- /dev/null +++ b/docs/stagingdemo.md @@ -0,0 +1,151 @@ +![alt text](./assets/images/staging.png "Staging from a raw table to the raw vault") + +We have two staging layers, as shown in the diagram above. + +## The raw staging layer + +First we create a raw staging layer. This feeds in data from the source system so that we can process it +more easily. In the ```models/raw``` folder we have provided two models which set up a raw staging layer. + +### raw_orders + +The ```raw_orders``` model feeds data from TPC-H, into a wide table containing all of the orders data +for a single day-feed. The day-feed will load data from the day given in the ```date``` var. + +### raw_inventory + +The ```raw_inventory``` model feeds the static inventory from TPC-H. As this data does not contain any dates, +we do not need to do any additional date processing or use the ```date``` var as we did for the raw orders data. +The inventory consists of the ```PARTSUPP```, ```SUPPLIER```, ```PART``` and ```LINEITEM``` tables. + +## Building the raw staging layer + +To build this layer with dbtvault, run the below command: + +```dbt run --models tag:raw``` + +Running this command will run all models which have the ``raw`` tag. We have given the ```raw``` tag to the +two raw staging layer models, so this will compile and run both models. + +The dbt output should give something like this: + +```shell +16:11:33 | Concurrency: 4 threads (target='dev') +16:11:33 | +16:11:33 | 1 of 2 START incremental model DEMO_RAW.raw_inventory................ [RUN] +16:11:33 | 2 of 2 START incremental model DEMO_RAW.raw_orders................... [RUN] +16:12:05 | 2 of 2 OK created incremental model DEMO_RAW.raw_orders.............. [SUCCESS 24627 in 32.46s] +16:12:43 | 1 of 2 OK created incremental model DEMO_RAW.raw_inventory........... [SUCCESS 8000000 in 69.54s] +16:12:43 | +16:12:43 | Finished running 2 incremental models in 81.39s. +``` + +## The hashed staging layer + +The tables in the raw staging layer need to be processed to add extra columns of data to make it ready +to load to the raw vault. + +Specifically, we need to add primary key hashes, hashdiffs, and any implied fixed-value columns +(see the diagram at the top of the page). + +We have created a number of macros for dbtvault, to make this step easier. Below are some links to +the macro documentation to provide a deeper understanding of how the macros work. + +- [multi-hash](macros.md#multi_hash) Generates SQL for hashes from lists of column/alias pairs. +- [add-columns](macros.md#add_columns) Generates SQL for additional columns with constant or function-derived values, +from lists of column/alias pairs. +- [from](macros.md#from) Generates SQL for selecting from the source table. + +## The model header + +For the staging layers we use a header as follows: + +```sql +{{- config(materialized='view', schema='STG', enabled=true, tags='stage') -}} +``` + +This header is fairly-straight forward and defines the model as a view, as well as defining the schema as ```STG``` +to ensure that the location we are materializing this model in makes sense in the overall system. + +We also define the ```stage``` tag to categorise this model and make it easier to isolate when +we want to only run staging layer models. + +## The source table + +In the ```v_stg_orders``` model, we use set the following ``source_table``` variable: + +```sql +{%- set source_table = ref('raw_orders') -%} +``` + +This allows us to make use of the additional functionality of the [add-columns](macros.md#add_columns) macro +which will enable it to automatically bring in all columns from the defined ```source_table```. + +This will be very convenient for when we need to access the data when creating the raw vault later. + +## Hashing + +We provide a number of column/alias pairs to the [multi-hash](macros.md#multi_hash) macro +to generate hashing SQL. These hashes will be used in the raw vault tables as primary key +and hashdiff fields. + +!!! note "Why do we hash?" + For more information on why we hash, refer to the [best practices](bestpractices.md#why-do-we-hash) page. + +For hashdiff columns, we provide an additional parameter, ```sort``` with the value ```true``` to get +dbtvault to sort the columns alphabetically when hashing, as per best practices. + +## Additional columns + +We also provide a number of column/alias pairs to the [add-columns](macros.md#add_columns) macro +to generate SQL for adding additional columns to our hashed stage view. + +AS we mentioned before, if the ```source_table``` variable we created is provided as the first parameter, +all of the ```source_table``` columns will automatically be selected. + +If there are any constants which overlap with the ```source_table```, and the ```source_table``` has been +provided as a parameter, the constants provided to this macro will take precedence. + +This macro can crate any number of extra columns, which may contain values generated by database function calls +or contain constant values provided by you, the user. + +There's a simple shorthand method for providing constants which you can observe being used in the hashed +staging models. If we provide a ```!``` in the string value, it will create a column with that string +(minus the ```!```) as its value in every row. This is very useful when defining a source, +as you may want to force it to a certain value for auditing purposes. + +## From + +This is a simple convenience macro which generates SQL in the form ```FROM ```. + +## The hashed staging models + +### v_stg_orders and v_stg_inventory + +The ```v_stg_orders``` and ```v_stg_inventory``` models use the raw layer's ```raw_orders``` and ```raw_inventory``` +models as sources, respectively. Both are created as views on the raw staging layer, as they are intended as +transformations on the data which already exists. + +Eeach view adds a number of primary keys, hashdiffs and additional constants for use in the raw vault. + +## Building the hashed staging layer + +To build this layer with dbtvault, run the below command: + +```dbt run --models tag:stage``` + +Running this command will run all models which have the ``stage`` tag. We have given the ```stage``` tag to the +two hashed staging layer models, so this will compile and run both models. + +The dbt output should give something like this: + +```shell +16:23:13 | Concurrency: 4 threads (target='dev') +16:23:13 | +16:23:13 | 1 of 2 START view model DEMO_STG.v_stg_inventory..................... [RUN] +16:23:14 | 2 of 2 START view model DEMO_STG.v_stg_orders........................ [RUN] +16:23:19 | 1 of 2 OK created view model DEMO_STG.v_stg_inventory................ [SUCCESS 1 in 5.10s] +16:23:20 | 2 of 2 OK created view model DEMO_STG.v_stg_orders................... [SUCCESS 1 in 5.10s] +16:23:20 | +16:23:20 | Finished running 2 view models in 13.27s. +``` \ No newline at end of file diff --git a/docs/stylesheets/cube.css b/docs/stylesheets/cube.css deleted file mode 100644 index d8693672b..000000000 --- a/docs/stylesheets/cube.css +++ /dev/null @@ -1,12 +0,0 @@ -/* Additional CSS to add styling to the navigation menu cube -and remove edit button */ - -.nav-cube img { - width: 90%; - padding: 0 .6rem; - margin-bottom: 20px; -} - -.md-content__icon { - display: none; -} \ No newline at end of file diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 000000000..8c8c6248c --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,37 @@ +/* Additional CSS to add styling to the navigation menu cube +and remove edit button */ + +.nav-cube img { + width: 90%; + padding: 0 .6rem; + margin-bottom: 20px; +} + +.md-content__icon { + display: none; +} + +/* Style buttons */ +.btn { + background-color: #91569D; + border: none; + color: white; + padding: 12px 30px; + cursor: pointer; + font-size: 20px; + +} + +.btn i, a.btn { + color: white; +} + +a.btn { + margin-top: 20px; + display: inline-block; +} + +/* Darker background on mouse-over */ +.btn:hover { + background-color: #7B4B87; +} \ No newline at end of file diff --git a/docs/workedexample.md b/docs/workedexample.md new file mode 100644 index 000000000..4a59149e9 --- /dev/null +++ b/docs/workedexample.md @@ -0,0 +1,40 @@ +## Introduction + +!!! info + The intent behind this demonstration is to give you further understanding of how + dbt and dbtvault could be used in a realistic environment. + For a more detailed guide on how to create your own Data Vault using dbtvault, + with a simplified example, take a look at our [walk-through](walkthrough.md) guide. + +In this section we teach you how to use dbtvault by example. We guide you through developing a +Data Vault 2.0 Data Warehouse based on the Snowflake TPC-H dataset, step-by-step using pre-written dbtvault models. + +We will: + +- setup a dbt project. +- examine and profile the TPCH dataset to explore how we can map it to the Data Vault architecture. +- create a raw staging layer. +- process the raw staging layer. +- create a Data Vault with hubs, links and satellites using dbtvault and pre-written models. + + +## Pre-requisites + +These pre-requisites are separate from those found on the [getting started](walkthrough.md) page and will +be the only necessary requirements you will need to get started with the example project. + +1. Some prior knowledge of Data Vault 2.0 architecture. Have a look at +[How can I get up to speed on Data Vault 2.0?](index.md#how-can-i-get-up-to-speed-on-data-vault-20) + +2. A Snowflake trial account. [Sign up for a free 30-day trial here](https://trial.snowflake.com/ab/) + +3. A Python 3.x installation. + +!!! warning + We suggest a trial account so that you have full privileges and assurance that the demo is isolated from any + production warehouses. Whilst there shouldn't be any risk that the demo affects any unrelated data outside of the + scope of this project, you may use a corporate account or existing personal account at your own risk, + +!!! note + We have provided a complete ```requirements.txt``` to install with ```pip install -r requirements.txt``` + as a quick way of getting your Python environment set up. This file includes dbt and comes with the download in the next section. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 438f14e33..c414690df 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,15 +18,20 @@ repo_url: 'https://github.com/Datavault-UK/dbtvault' nav: - Home: 'index.md' - - Getting Started: 'gettingstarted.md' - - Best Practices: 'bestpractices.md' - - Loading the vault: + - Walk-through guide: + - Getting Started: 'walkthrough.md' - Staging: 'staging.md' - Hubs: 'hubs.md' - Links: 'links.md' - Satellites: 'satellites.md' + - Worked example: + - Getting Started: 'workedexample.md' + - Project setup: 'setup.md' + - Profiling TPC-H: 'sourceprofile.md' + - Creating the stage layers: 'stagingdemo.md' + - Loading the vault: 'loading.md' - Macros: 'macros.md' - - Demonstration: 'demonstration.md' + - Best Practices: 'bestpractices.md' - Roadmap: 'roadmap.md' - Changelog: 'changelog.md' - Contributing: 'contributing.md' @@ -55,7 +60,11 @@ markdown_extensions: permalink: true toc_depth: 1-3 +#plugins: +# - search +# - pdf-export + extra_css: - - 'stylesheets/cube.css' + - 'stylesheets/extra.css' copyright: dbtvault and documentation © Business Thinking trading as Datavault 2019 - Data Vault 2.0 is a registered trademark of Dan Linstedt - dbt is a registered trademark of Fishtown Analytics From 110bb1525e804315c4551511b43feb379ad62193 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 31 Oct 2019 15:08:49 +0000 Subject: [PATCH 084/157] Added missing page and updated versions --- README.md | 4 +-- dbt_project.yml | 2 +- docs/changelog.md | 8 +++++ docs/walkthrough.md | 73 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 docs/walkthrough.md diff --git a/README.md b/README.md index e77bd65ab..33bf4f96c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.2-pre)](https://dbtvault.readthedocs.io/en/v0.3.2-pre/?badge=v0.3.2-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.3-pre)](https://dbtvault.readthedocs.io/en/v0.3.3-pre/?badge=v0.3.3-pre) [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) @@ -34,7 +34,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.3.2-pre # Latest stable version + revision: v0.3.3-pre # Latest stable version ``` And run ```dbt deps``` diff --git a/dbt_project.yml b/dbt_project.yml index 50cf18969..98037123b 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,5 +1,5 @@ name: 'dbtvault' -version: '0.3.2' +version: '0.3.3' profile: 'dbtvault' diff --git a/docs/changelog.md b/docs/changelog.md index 1e49ef888..e23aad4d4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.3.3-pre] - 2019-10-31 +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.3-pre)](https://dbtvault.readthedocs.io/en/v0.3.3-pre/?badge=v0.3.3-pre) + +### Documentation + +- Added full demonstration project/worked example, using snowflake. +- Minor corrections + ## [v0.3.2-pre] - 2019-10-28 [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.2-pre)](https://dbtvault.readthedocs.io/en/v0.3.2-pre/?badge=v0.3.2-pre) diff --git a/docs/walkthrough.md b/docs/walkthrough.md new file mode 100644 index 000000000..d62d41b10 --- /dev/null +++ b/docs/walkthrough.md @@ -0,0 +1,73 @@ +## Introduction + +!!! info + This walk-through intends to give you a detailed understanding of how to use + dbtvault and the provided macros to develop a Data Vault Data Warehouse from the ground up. + If you're looking to quickly experiment and learn using pre-written models, + take a look at our [worked example](workedexample.md). + +In this section we teach you how to use dbtvault step-by-step, explaining the use of macros and the +different components of the Data Vault in detail. + +We will: + +- process a raw staging layer. +- create a Data Vault with hubs, links and satellites using dbtvault. + +## Prerequisites + +1. Some prior knowledge of Data Vault 2.0 architecture. Have a look at +[How can I get up to speed on Data Vault 2.0?](index.md#how-can-i-get-up-to-speed-on-data-vault-20) + +2. A Snowflake account, trial or otherwise. [Sign up for a free 30-day trial here](https://trial.snowflake.com/ab/) + +3. You must have downloaded and installed dbt, and [set up a project](https://docs.getdbt.com/docs/dbt-projects). + +4. Sources should be set up in dbt [(see below)](#setting-up-sources). + +5. We assume you already have a raw staging layer. + +6. Our macros assume that you are only loading from one set of load dates in a single load cycle (i.e. your staging layer +contains data for one ```load_datetime``` value only). **We will be removing this restriction in future releases**. + +7. You should read our [best practices](bestpractices.md) guidance. + +## Setting up sources + +We will be using the ```source``` feature of dbt extensively throughout the documentation to make access to source +data much easier, cleaner and more modular. + +We have provided an example below which shows a configuration similar to that used for the examples in our documentation, +however this feature is documented extensively in [the documentation for dbt](https://docs.getdbt.com/docs/using-sources). + +After reading the above documentation, we recommend that you place the ```schema.yml``` file you create for your sources, +in the root of your ```models``` folder, however you can place it where needed for your specific project and models. + +```schema.yml``` + +```yaml +version: 2 + +sources: + - name: MYSOURCE + database: MYDATABASE + schema: MYSCHEMA + tables: + - name: stg_customer # alias + identifier: stg_customer_hashed # table name + - name: ... +``` + +## Installation + +Add the following to your ```packages.yml```: + +```yaml +packages: + + - git: "https://github.com/Datavault-UK/dbtvault" +``` +And run +```dbt deps``` + +[Read more on package installation (from dbt)](https://docs.getdbt.com/docs/package-management) \ No newline at end of file From 4a83508f09bc3f23ce03dab03072db0456f81eae Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 31 Oct 2019 15:15:01 +0000 Subject: [PATCH 085/157] Added worked example to readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 33bf4f96c..8d01c8c73 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,13 @@ What does dbtvault offer? powered by [dbt](https://www.getdbt.com/), a registered trademark of [Fishtown Analytics](https://www.fishtownanalytics.com/) +## Worked example project + +Get started quickly with our worked example + +[read the docs](https://dbtvault.readthedocs.io/en/latest/workedexample/) +[Repository](https://github.com/Datavault-UK/snowflakeDemo) + ## Currently supported databases: - [snowflake](https://www.snowflake.com/about/) From 9ce0335114a033f70a798e71e60ca8f060b6c3d1 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 31 Oct 2019 15:16:40 +0000 Subject: [PATCH 086/157] Fixed Readme --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8d01c8c73..3b5c93d43 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,11 @@ powered by [dbt](https://www.getdbt.com/), a registered trademark of [Fishtown A ## Worked example project -Get started quickly with our worked example +Get started quickly with our worked example: -[read the docs](https://dbtvault.readthedocs.io/en/latest/workedexample/) -[Repository](https://github.com/Datavault-UK/snowflakeDemo) +- [Read the docs](https://dbtvault.readthedocs.io/en/latest/workedexample/) + +- [Project Repository](https://github.com/Datavault-UK/snowflakeDemo) ## Currently supported databases: From e23be07305c7072b92410a36f7e929f9922e2684 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 1 Nov 2019 09:22:37 +0000 Subject: [PATCH 087/157] Removed demo from roadmap It has been released now --- docs/roadmap.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index b610c0880..03827725c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -13,7 +13,6 @@ We will be releasing changes incrementally, so you can reap the benefits as soon These features are currently planned for the near-future. -- Full Snowflake TPC-H Demonstration to supplement the documentation - Transactional Links (Also known as non-historised links) ## Future releases From c8192cdec524d386d6f4598c9ebf728d5b92d6a6 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 1 Nov 2019 14:09:31 +0000 Subject: [PATCH 088/157] Added performance note to worked example --- docs/staging.md | 2 +- docs/workedexample.md | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/staging.md b/docs/staging.md index 1dc18b445..8e18e9c6a 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -63,7 +63,7 @@ in our model. !!! note On line 3 below we are using a dbt source. - If you have not yet set up sources in your dbt configuration please refer to [setting up sources](gettingstarted.md#setting-up-sources). + If you have not yet set up sources in your dbt configuration please refer to [setting up sources](walkthrough.md#setting-up-sources). ```stg_customer_hashed.sql``` diff --git a/docs/workedexample.md b/docs/workedexample.md index 4a59149e9..1ee127306 100644 --- a/docs/workedexample.md +++ b/docs/workedexample.md @@ -17,7 +17,6 @@ We will: - process the raw staging layer. - create a Data Vault with hubs, links and satellites using dbtvault and pre-written models. - ## Pre-requisites These pre-requisites are separate from those found on the [getting started](walkthrough.md) page and will @@ -37,4 +36,25 @@ be the only necessary requirements you will need to get started with the example !!! note We have provided a complete ```requirements.txt``` to install with ```pip install -r requirements.txt``` - as a quick way of getting your Python environment set up. This file includes dbt and comes with the download in the next section. \ No newline at end of file + as a quick way of getting your Python environment set up. This file includes dbt and comes with the download in the + next section. + +## Performance note + +Please be aware that table structures are simulated from the TPCH-H dataset. The TPC-H dataset is a static view of data. + +Only a subset of the data contains dates which allows us to simulate daily feeds. The ```v_stg_orders``` orders view is +filtered by date, unfortunately the ```v_stg_inventory``` view cannot be filtered by date, so it ends up being a feed of +the entire contents of the view each cycle. + +This means that inventory related hubs links and satellites are populated once during the initial load cycle with +everything and later cycles insert 0 new records in their left outer joins. + +As the dataset increases in size, e.g if you run with a larger TPC-H dataset (100, 1000 etc.) then be aware you are +processing the entire inventory dataset each cycle, which results in unrepresentative load cycle times. + +Unfortunately it's the nature of the dataset, it will not be that way for other datasets. We will look at additonal +datasets in the future! + +If you are feeling adventurous you may disable the inventory feed (```raw_inventory``` and child models) to see a more +accurate representation of performance. \ No newline at end of file From 07ea7ee168000e29e31dde85de20109a43b076ff Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 27 Nov 2019 10:11:16 +0000 Subject: [PATCH 089/157] Added UKDVUG announcement --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 3b5c93d43..e01f8d300 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ +

+ +

There will be a live demonstration of dbtvault at the next UK Data Vault User Group on Tuesday, December 3, 2019 @ 6pm in LONDON. + + Sign up for FREE now! +

+

+

From 8fe101110219eca10f7057fd0ed5ab77bb00708a Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 27 Nov 2019 15:01:06 +0000 Subject: [PATCH 090/157] Version 0.4 release Added: - Table Macros: - Transactional Links Improved: - Hashing: - You may now choose between MD5 and SHA-256 hashing with a simple yaml configuration Worked example: - Transactional Links - Added a transactional link model using a simulated transaction feed. Documentation: - Updated macros, best practices, roadmap, and other pages to account for new features - Updated worked example documentation - Replaced all dbt documentation links with links to the 0.14 documentation as dbtvault is using dbt 0.14 currently (we will be updating to 0.15 soon!) - Minor corrections --- CONTRIBUTING.md | 15 +-- README.md | 9 +- dbt_project.yml | 6 +- docs/bestpractices.md | 48 +++++++- docs/changelog.md | 28 ++++- docs/contributing.md | 3 +- docs/hubs.md | 6 +- docs/links.md | 2 +- docs/loading.md | 30 ++++- docs/macros.md | 142 ++++++++++++++++++++--- docs/roadmap.md | 4 +- docs/satellites.md | 10 +- docs/setup.md | 2 +- docs/sourceprofile.md | 24 +++- docs/stagingdemo.md | 52 ++++++--- docs/t_links.md | 182 ++++++++++++++++++++++++++++++ docs/walkthrough.md | 7 +- docs/workedexample.md | 19 ++-- macros/supporting/hash.sql | 22 +++- macros/tables/t_link_template.sql | 45 ++++++++ mkdocs.yml | 1 + 21 files changed, 575 insertions(+), 82 deletions(-) create mode 100644 docs/t_links.md create mode 100644 macros/tables/t_link_template.sql diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 002b24959..0b251dae9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,16 @@ ## We'd love to hear from you -This dbtvault package is very much a work in progress – we’ll up the version number to 1.0 when we’re satisfied it -works out in the wild. +dbtvault is very much a work in progress – we’re constantly adding quality of life improvements and will be adding +new table types regularly. We know that it deserves new features, that the code base can be tidied up and the SQL better tuned. -Rest assured we’re working on it for future releases – our roadmap contains information on what’s coming. -If you spot anything you’d like to bring to our attention, have a request for new features, -have spotted an improvement we could make, or want to tell us about a typo, then please don’t hesitate to let us know -by submitting an issue using the below guidelines +Rest assured we’re working on it for future releases – [our roadmap contains information on what’s coming](roadmap.md). + +If you spot anything you’d like to bring to our attention, have a request for new features, have spotted an improvement we could make, +or want to tell us about a typo or bug, then please don’t hesitate to let us know via [Github](https://github.com/Datavault-UK/dbtvault/issues). -We’d rather know you are making active use of this package than hearing nothing from all of you out there! +We’d rather know you are making active use of this package than hearing nothing from all of you out there! Happy Data Vaulting! @@ -20,6 +20,7 @@ Happy Data Vaulting! We've tested the package rigorously, but if you think you've found a bug please provide the following at a minimum (or use the issue templates) so we can fix it as quickly as possible: +- The version of dbt being used - The version of dbtvault being used. - Steps to reproduce the issue - Any error messages or dbt log files which can give more detail of the problem diff --git a/README.md b/README.md index e01f8d300..3f28b699d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.3-pre)](https://dbtvault.readthedocs.io/en/v0.3.3-pre/?badge=v0.3.3-pre) +stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4)](https://dbtvault.readthedocs.io/en/v0.4/?badge=v0.4) [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) @@ -43,6 +43,7 @@ Get started quickly with our worked example: ## Installation +Ensure you are using dbt 0.14 (0.15 support will be added soon!) Add the following to your ```packages.yml``` @@ -50,12 +51,12 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.3.3-pre # Latest stable version + revision: v0.4 # Latest stable version ``` And run ```dbt deps``` -[Read more on package installation](https://docs.getdbt.com/docs/package-management) +[Read more on package installation](https://docs.getdbt.com/v0.14.0/docs/package-management) ## Usage @@ -84,4 +85,4 @@ before anyone else! [View our contribution guidelines](CONTRIBUTING.md) ## License -[Apache 2.0](LICENSE.md) +[Apache 2.0](LICENSE.md) \ No newline at end of file diff --git a/dbt_project.yml b/dbt_project.yml index 98037123b..8e00e9e5d 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,5 +1,5 @@ name: 'dbtvault' -version: '0.3.3' +version: '0.4' profile: 'dbtvault' @@ -13,3 +13,7 @@ target-path: "target" clean-targets: - "target" - "dbt_modules" + +models: + vars: + hash: MD5 \ No newline at end of file diff --git a/docs/bestpractices.md b/docs/bestpractices.md index 02f5a4b02..b898e91a0 100644 --- a/docs/bestpractices.md +++ b/docs/bestpractices.md @@ -43,8 +43,9 @@ then there is a chance of a clash: where two different values generate the same For this reason, it **should not be** used for cryptographic purposes either. -In future releases of dbtvault, we will allow you to change the algorithm that is used (e.g. to SHA-256) to reduce the -chance of a clash (at the expense of more processing and a larger column), or switch off hashing entirely. +!!! success + + You may now choose between MD5 and SHA-256 in dbtvault, [read below](bestpractices.md#choosing-a-hashing-algorithm-in-dbtvault). ### Why do we hash? @@ -80,4 +81,45 @@ staging tables the sorting functionality for primary keys. - For **links**, columns must be sorted by the primary key of the hub and arranged alphabetically by the hub name. -The order must also be the same as each hub. \ No newline at end of file +The order must also be the same as each hub. + +### Choosing a hashing algorithm in dbtvault + +With the release of dbtvault 0.4, you may now choose between ```MD5``` and ```SHA-256``` hashing. ```SHA-256``` was added +to dbtvault as an option for users who wish to reduce the hashing collision rates in larger data sets. + +!!! note + + If a hashing algorithm configuration is missing or invalid, dbtvault will use ```MD5``` by default. + +Configuring the hashing algorithm which will be used by dbtvault is simple: simply add a variable to your +```dbt_project.yml``` as follows: + +```dbt_project.yml``` +```yaml + +name: 'my_project' +version: '1' + +profile: 'my_project' + +source-paths: ["models"] +analysis-paths: ["analysis"] +test-paths: ["tests"] +data-paths: ["data"] +macro-paths: ["macros"] + +target-path: "target" +clean-targets: + - "target" + - "dbt_modules" + +models: + vars: + hash: SHA # or MD5 +``` + +It is possible to configure a hashing algorithm on a model-by-model basis using the hierarchical structure of the ```yaml``` file. +We recommend you keep the hashing algorithm consistent across all tables, however, as per best practise. + +Read the [dbt documentation](https://docs.getdbt.com/v0.14.0/docs/var) for further information on variable scoping. \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index e23aad4d4..7ad437f88 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.4] - 2019-11-27 +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4)](https://dbtvault.readthedocs.io/en/v0.4-pre/?badge=v0.4) + +### Added + +- Table Macros: + - [Transactional Links](macros.md#t_link_template) + +### Improved + +- Hashing: + - You may now choose between ```MD5``` and ```SHA-256``` hashing with a simple yaml configuration + [Learn how!](bestpractices.md#choosing-a-hashing-algorithm-in-dbtvault) + +### Worked example + +- Transactional Links + - Added a transactional link model using a simulated transaction feed. + +### Documentation + +- Updated macros, best practices, roadmap, and other pages to account for new features +- Updated worked example documentation +- Replaced all dbt documentation links with links to the 0.14 documentation as dbtvault +is using dbt 0.14 currently (we will be updating to 0.15 soon!) +- Minor corrections + ## [v0.3.3-pre] - 2019-10-31 [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.3.3-pre)](https://dbtvault.readthedocs.io/en/v0.3.3-pre/?badge=v0.3.3-pre) @@ -131,7 +158,6 @@ the new and improved features. ### Added - - Table Macros: - [Hub](macros.md#hub_template) - [Link](macros.md#link_template) diff --git a/docs/contributing.md b/docs/contributing.md index 3c4667faf..54c04e74b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -8,7 +8,7 @@ We know that it deserves new features, that the code base can be tidied up and t Rest assured we’re working on it for future releases – [our roadmap contains information on what’s coming](roadmap.md). If you spot anything you’d like to bring to our attention, have a request for new features, have spotted an improvement we could make, -or want to tell us about a typo, then please don’t hesitate to let us know via [Github](https://github.com/Datavault-UK/dbtvault/issues). +or want to tell us about a typo or bug, then please don’t hesitate to let us know via [Github](https://github.com/Datavault-UK/dbtvault/issues). We’d rather know you are making active use of this package than hearing nothing from all of you out there! @@ -20,6 +20,7 @@ Happy Data Vaulting! :smile: We've tested the package rigorously, but if you think you've found a bug please provide the following at a minimum (or use the issue templates) so we can fix it as quickly as possible: +- The version of dbt being used - The version of dbtvault being used. - Steps to reproduce the issue - Any error messages or dbt log files which can give more detail of the problem diff --git a/docs/hubs.md b/docs/hubs.md index ba67b59a9..653f8dffc 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -26,7 +26,7 @@ The following header is what we use, but feel free to customise it to your needs Hubs are always incremental, as we load and add new records to the existing data set. -[Read more about incremental models](https://docs.getdbt.com/docs/configuring-incremental-models) +[Read more about incremental models](https://docs.getdbt.com/v0.14.0/docs/configuring-incremental-models) !!! note "Dont worry!" The [hub_template](macros.md#hub_template) deals with the Data Vault @@ -39,10 +39,10 @@ Let's look at the metadata we need to provide to the [hub_template](macros.md#hu #### Source table The first piece of metadata we need is the source table. This step is easy, as in this example we created the -new staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. +staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. dbt ensures dependencies are honoured when defining the source using a reference in this way. -[Read more about the ref function](https://docs.getdbt.com/docs/ref) +[Read more about the ref function](https://docs.getdbt.com/v0.14.0/docs/ref) ```hub_customer.sql``` diff --git a/docs/links.md b/docs/links.md index 7827b13cc..58a06b196 100644 --- a/docs/links.md +++ b/docs/links.md @@ -38,7 +38,7 @@ The first piece of metadata we need is the source table. This step is easy, as w staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. dbt ensures dependencies are honoured when defining the source using a reference in this way. -[Read more about the ref function](https://docs.getdbt.com/docs/ref) +[Read more about the ref function](https://docs.getdbt.com/v0.14.0/docs/ref) ```link_customer_nation.sql``` diff --git a/docs/loading.md b/docs/loading.md index b343752e4..c297d3561 100644 --- a/docs/loading.md +++ b/docs/loading.md @@ -29,11 +29,11 @@ This will run all models with the hub tag. Links are another fundamental component in a Data Vault. Links model an association or link, between two business keys. They commonly hold business transactions or structural -information. +information. A link specifically contains the structural information. Our links will contain: -1. A primary key. For links, we take the natural keys (prior to hashing) represented by the foreign key columns below +1. A primary key. For links, we take the natural keys (prior to hashing) represented by the foreign key columns and create a hash on a concatenation of them. 2. Foreign keys holding the primary key for each hub referenced in the link (2 or more depending on the number of hubs referenced) @@ -89,6 +89,32 @@ To compile and load the provided satellite models, run the following command: This will run all models with the satellite tag. +## Transactional Links + +Transactional Links are used to model transactions between entities in a Data Vault. + +Links model an association or link, between two business keys. They commonly hold business transactions or structural +information. A transactional link specifically contains the business transactions. + +Our transactional links will contain: + +1. A primary key. For transactional links, we use the transaction number. If this is not already present in the dataset +then we create this by concatenating the foreign keys and hashing them. +2. Foreign keys holding the primary key for each hub referenced in the transactional link (2 or more depending on the number of hubs +referenced) +3. A payload. This will be data about the transaction itself e.g. the amount, type, date or non-hashed transaction number. +4. An ```EFFECTIVE_FROM``` date. This will usually be the date of the transaction. +5. The load date or load date timestamp. +6. The source for the record + +### Loading transactional links + +To compile and load the provided t_link models, run the following command: + +```dbt run --models tag:t_link``` + +This will run all models with the t_link tag. + ## Loading the full system Each of the commands above load a particular type of table, however, we may want to do a full system load. diff --git a/docs/macros.md b/docs/macros.md index 026513f39..3000b13a5 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -7,10 +7,6 @@ for your Data Vault. ### Metadata notes #### Using a source reference for the target metadata -!!! note - As of release 0.3, you may now use a source reference as a target metadata value, to streamline metadata entry. - Read below! - In the usage examples for the table template macros in this section, you will see ```source``` provided as the values for some of the target metadata variables. ```source``` has been declared as a variable at the top of the models, and holds a reference to the source table we are loading from. This is shorthand for retaining the name and data types @@ -399,6 +395,88 @@ WHERE src.HASHDIFF IS NULL ___ +### t_link_template + +Generates sql to build a transactional link table using the provided metadata. + +```mysql +dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, + source) +``` + +#### Parameters + +| Parameter | Description | Type | Required? | +| ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | check_circle | +| src_fk | Source foreign key column(s) | List | check_circle | +| src_payload | Source payload column(s) | List | check_circle | +| src_eff | Source effective from column | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | check_circle | +| tgt_fk | Target hashdiff column | List/Reference | check_circle | +| tgt_payload | Target foreign key column(s) | List/Reference | check_circle | +| tgt_eff | Target effective from column | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | +| source | Staging model reference or table name | List/Reference | check_circle | + +#### Usage + + +``` yaml + +-- t_link_transactions.sql: + +{{- config(...) -}} + +{%- set source = [ref('stg_transactions_hashed')] -%} + +{%- set src_pk = 'TRANSACTION_PK' -%} +{%- set src_fk = ['CUSTOMER_FK', 'ORDER_FK'] -%} +{%- set src_payload = ['TRANSACTION_NUMBER', 'TRANSACTION_DATE', 'TYPE', 'AMOUNT'] -%} +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_fk = source -%} +{%- set tgt_payload = source -%} +{%- set tgt_eff = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, + source) }} +``` + +#### Output + +```mysql +SELECT DISTINCT + CAST(stg.TRANSACTION_PK AS BINARY) AS TRANSACTION_PK, + CAST(stg.CUSTOMER_FK AS BINARY) AS CUSTOMER_FK, + CAST(stg.ORDER_FK AS BINARY) AS ORDER_FK, + CAST(stg.TRANSACTION_NUMBER AS NUMBER(38,0)) AS TRANSACTION_NUMBER, + CAST(stg.TRANSACTION_DATE AS DATE) AS TRANSACTION_DATE, + CAST(stg.TYPE AS VARCHAR) AS TYPE, + CAST(stg.AMOUNT AS NUMBER(12,2)) AS AMOUNT, + CAST(stg.EFFECTIVE_FROM AS DATE) AS EFFECTIVE_FROM, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR) AS SOURCE +FROM ( + SELECT stg.TRANSACTION_PK, stg.CUSTOMER_FK, stg.ORDER_FK, stg.TRANSACTION_NUMBER, stg.TRANSACTION_DATE, stg.TYPE, stg.AMOUNT, stg.EFFECTIVE_FROM, stg.LOADDATE, stg.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_transactions_hashed AS stg +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.t_link_transactions AS tgt +ON stg.TRANSACTION_PK = tgt.TRANSACTION_PK +WHERE tgt.TRANSACTION_PK IS NULL +``` +___ + ## Staging Macros ######(macros/staging) @@ -410,20 +488,28 @@ ___ !!! warning This macro ***should not be*** used for cryptographic purposes. - The intended use is for creating checksum-like fields only, so that a record change can be detected. + The intended use is for creating checksum-like values only, so that we may compare records accurately. [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) !!! seealso "See Also" - [hash](#hash) - [Hashing best practises and why we hash](bestpractices.md#hashing) + - With the release of dbtvault 0.4, you may now choose between ```MD5``` and ```SHA-256``` hashing. + [Learn how](bestpractices.md#choosing-a-hashing-algorithm-in-dbtvault) This macro will generate SQL hashing sequences for one or more columns as below: -```sql + +```sql tab='MD5' CAST(MD5_BINARY(UPPER(TRIM(CAST(column1 AS VARCHAR)))) AS BINARY(16)) AS alias1, CAST(MD5_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(16)) AS alias2 ``` +```sql tab='SHA' +CAST(SHA2_BINARY(UPPER(TRIM(CAST(column1 AS VARCHAR)))) AS BINARY(32)) AS alias1, +CAST(SHA2_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(32)) AS alias2 +``` + #### Parameters | Parameter | Description | Type | Required? | @@ -444,14 +530,24 @@ CAST(MD5_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(16)) AS alias2 #### Output -```mysql +```mysql tab='MD5' CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, CAST(MD5_BINARY(CONCAT( - IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) AS BINARY(16)) AS HASHDIFF + IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) AS BINARY(16)) AS HASHDIFF +``` + +```mysql tab='SHA' +CAST(SHA2_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(32)) AS CUSTOMER_PK, + +CAST(SHA2_BINARY(CONCAT( + IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) AS BINARY(32)) AS HASHDIFF ``` !!! success "Column sorting" @@ -542,7 +638,7 @@ FROM MYDATABASE.MYSCHEMA.MYTABLE ``` !!! info - Sources need to be set up in dbt to ensure this works. [Read More](https://docs.getdbt.com/docs/using-sources) + Sources need to be set up in dbt to ensure this works. [Read More](https://docs.getdbt.com/v0.14.0/docs/using-sources) #### Parameters @@ -626,19 +722,26 @@ ___ !!! warning This macro ***should not be*** used for cryptographic purposes. - The intended use is for creating checksum-like fields only, so that a record change can be detected. + The intended use is for creating checksum-like values only, so that we may compare records accurately. [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) !!! seealso "See Also" - [multi-hash](#multi_hash) - [Hashing best practises and why we hash](bestpractices.md#hashing) + - With the release of dbtvault 0.4, you may now choose between ```MD5``` and ```SHA-256``` hashing. + [Learn how](bestpractices.md#choosing-a-hashing-algorithm-in-dbtvault) A macro for generating hashing SQL for columns: -```sql + +```sql tab='MD5' CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias ``` +```sql tab='SHA' +CAST(SHA2_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(32)) AS alias +``` + - Can provide multiple columns as a list to create a concatenated hash - Columns are sorted alphabetically (by alias) if you set the ```sort``` flag to true. - Generally, you should alpha sort hashdiffs using the ```sort``` flag. @@ -667,7 +770,7 @@ CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias #### Output -```mysql +```mysql tab = 'MD5' CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', @@ -676,6 +779,15 @@ CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), AS BINARY(16)) AS HASHDIFF ``` +```mysql tab='SHA' +CAST(SHA2_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(32)) AS CUSTOMER_PK, +CAST(SHA2_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) + AS BINARY(32)) AS HASHDIFF +``` + ___ ### prefix diff --git a/docs/roadmap.md b/docs/roadmap.md index 03827725c..25d14a095 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -13,7 +13,7 @@ We will be releasing changes incrementally, so you can reap the benefits as soon These features are currently planned for the near-future. -- Transactional Links (Also known as non-historised links) +- Effectivity satellites ## Future releases @@ -22,9 +22,9 @@ In future releases, we hope to include the following: ### Tables - Multi-active satellites -- Effectivity satellites - Status tracking satellites - Point-in-Time tables (also know as PITs) - Bridge tables - Reference Tables +- Mart loading helpers - And more! \ No newline at end of file diff --git a/docs/satellites.md b/docs/satellites.md index 0cd252e8d..69c651f2e 100644 --- a/docs/satellites.md +++ b/docs/satellites.md @@ -42,7 +42,7 @@ The following header is what we use, but feel free to customise it to your needs Satellites are always incremental, as we load and add new records to the existing data set. -[Read more about incremental models](https://docs.getdbt.com/docs/configuring-incremental-models) +[Read more about incremental models](https://docs.getdbt.com/v0.14.0/docs/configuring-incremental-models) ### Adding the metadata @@ -51,10 +51,10 @@ Let's look at the metadata we need to provide to the [sat_template](macros.md#sa #### Source table The first piece of metadata we need is the source table. This step is easy, as in this example we created the -new staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. +staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. dbt ensures dependencies are honoured when defining the source using a reference in this way. -[Read more about the ref function](https://docs.getdbt.com/docs/ref) +[Read more about the ref function](https://docs.getdbt.com/v0.14.0/docs/ref) ```sat_customer_details.sql``` ```sql hl_lines="3" @@ -201,6 +201,4 @@ And our table will look like this: ### Next steps -We have now created a staging layer and a hub, link and satellite. We'll be bringing new -table structures in future releases. We'll also be releasing material which demonstrates these examples in a live -environment soon! \ No newline at end of file +We have now created a staging layer and a hub, link and satellite. Next we will ook at transactional links. \ No newline at end of file diff --git a/docs/setup.md b/docs/setup.md index e7b787b62..e104073cd 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -36,7 +36,7 @@ In your dbt profiles, you must create a connection with this name and provide th account details so that dbt can connect to your Snowflake databases. dbt provides their own documentation on how to configure profiles, so we suggest reading that -[here](https://docs.getdbt.com/docs/configure-your-profile). +[here](https://docs.getdbt.com/v0.14.0/docs/configure-your-profile). A sample profile configuration is provided below which will get you started: diff --git a/docs/sourceprofile.md b/docs/sourceprofile.md index cef483d15..d4fb1df52 100644 --- a/docs/sourceprofile.md +++ b/docs/sourceprofile.md @@ -53,6 +53,25 @@ Next we looked at the relationship between customers and orders. We wanted to ch We did this by doing a left outer join on the ```ORDERS``` table, with the ```CUSTOMER``` table and discovered that several customers exist without orders. +#### Transactions + +To create transactional links in the demonstration project, we needed to simulate transactions, as there are no suitable +or explicit transaction records present in the dataset. There are implied transactions however, as customers place orders. +To simulate a concrete transactions, we created a raw staging layer as a view, called +```raw_transactions``` and used the following fields: + +- Customer key +- Order key +- Order date +- Total price, aliased as Amount, to mean the order is paid off in full. +- Type, a generated column, using a random selection between ```CR``` or ```DR``` to mean a debit or credit to the customer. +- Transaction Date. A calculated column which is takes the order date and adds 20 days, to mean a customer paid 20 days +after their order was made. +- Transaction number. A calculated column created by concatenating the Order key, Customer key and order date and padding the +result with 0s to ensure the number is 24 digits long. + +The ```ORDERS``` and ```CUSTOMER``` tables are then joined (left outer) to simulate transactions on customer orders. + ### Conclusions To create a source feed simulation with the static data (shown by the logical pattern in the date fields), we can use @@ -69,7 +88,10 @@ to the ```LINEITEM``` table. The relationship between customers and orders tells us that customers without an order will not be loaded into the Data Vault, as we are using the ```ORDERDATE``` for day-feed simulation. -Now that we have profiled the data, we cna make more informed decisions when mapping the source system to the Data Vault +This also means that we can simulate transactions by using the implication that a customer makes a payment on an order +some time after the order has been made. + +Now that we have profiled the data, we can make more informed decisions when mapping the source system to the Data Vault architecture. diff --git a/docs/stagingdemo.md b/docs/stagingdemo.md index f58170f2c..cc1ea8ae2 100644 --- a/docs/stagingdemo.md +++ b/docs/stagingdemo.md @@ -18,6 +18,13 @@ The ```raw_inventory``` model feeds the static inventory from TPC-H. As this dat we do not need to do any additional date processing or use the ```date``` var as we did for the raw orders data. The inventory consists of the ```PARTSUPP```, ```SUPPLIER```, ```PART``` and ```LINEITEM``` tables. +### raw_transactions + +The ```raw_inventory``` simulates transactions so that we can create transactional links. It does this by +making a number of calculations on orders made by customers and creating transaction records. + +[Read more](sourceprofile.md#transactions) + ## Building the raw staging layer To build this layer with dbtvault, run the below command: @@ -30,14 +37,17 @@ two raw staging layer models, so this will compile and run both models. The dbt output should give something like this: ```shell -16:11:33 | Concurrency: 4 threads (target='dev') -16:11:33 | -16:11:33 | 1 of 2 START incremental model DEMO_RAW.raw_inventory................ [RUN] -16:11:33 | 2 of 2 START incremental model DEMO_RAW.raw_orders................... [RUN] -16:12:05 | 2 of 2 OK created incremental model DEMO_RAW.raw_orders.............. [SUCCESS 24627 in 32.46s] -16:12:43 | 1 of 2 OK created incremental model DEMO_RAW.raw_inventory........... [SUCCESS 8000000 in 69.54s] -16:12:43 | -16:12:43 | Finished running 2 incremental models in 81.39s. +14:18:17 | Concurrency: 4 threads (target='dev') +14:18:17 | +14:18:17 | 1 of 3 START view model DEMO_RAW.raw_inventory....................... [RUN] +14:18:17 | 2 of 3 START view model DEMO_RAW.raw_orders.......................... [RUN] +14:18:17 | 3 of 3 START view model DEMO_RAW.raw_transactions.................... [RUN] +14:18:19 | 3 of 3 OK created view model DEMO_RAW.raw_transactions............... [SUCCESS 1 in 1.49s] +14:18:19 | 1 of 3 OK created view model DEMO_RAW.raw_inventory.................. [SUCCESS 1 in 1.71s] +14:18:20 | 2 of 3 OK created view model DEMO_RAW.raw_orders..................... [SUCCESS 1 in 2.06s] +14:18:20 | +14:18:20 | Finished running 3 view models in 8.10s. + ``` ## The hashed staging layer @@ -126,7 +136,13 @@ The ```v_stg_orders``` and ```v_stg_inventory``` models use the raw layer's ```r models as sources, respectively. Both are created as views on the raw staging layer, as they are intended as transformations on the data which already exists. -Eeach view adds a number of primary keys, hashdiffs and additional constants for use in the raw vault. +Each view adds a number of primary keys, hashdiffs and additional constants for use in the raw vault. + +### v_stg_transactions + +The ```v_stg_transactions``` model uses the raw layer's ```raw_transactions``` model as its source. +For the load date, we add a day to the ```TRANSACTION_DATE``` to simulate the fact we are loading the data in the date +after the transaction was made. ## Building the hashed staging layer @@ -140,12 +156,14 @@ two hashed staging layer models, so this will compile and run both models. The dbt output should give something like this: ```shell -16:23:13 | Concurrency: 4 threads (target='dev') -16:23:13 | -16:23:13 | 1 of 2 START view model DEMO_STG.v_stg_inventory..................... [RUN] -16:23:14 | 2 of 2 START view model DEMO_STG.v_stg_orders........................ [RUN] -16:23:19 | 1 of 2 OK created view model DEMO_STG.v_stg_inventory................ [SUCCESS 1 in 5.10s] -16:23:20 | 2 of 2 OK created view model DEMO_STG.v_stg_orders................... [SUCCESS 1 in 5.10s] -16:23:20 | -16:23:20 | Finished running 2 view models in 13.27s. +14:19:17 | Concurrency: 4 threads (target='dev') +14:19:17 | +14:19:17 | 1 of 3 START view model DEMO_STG.v_stg_inventory..................... [RUN] +14:19:17 | 2 of 3 START view model DEMO_STG.v_stg_orders........................ [RUN] +14:19:17 | 3 of 3 START view model DEMO_STG.v_stg_transactions.................. [RUN] +14:19:19 | 3 of 3 OK created view model DEMO_STG.v_stg_transactions............. [SUCCESS 1 in 1.99s] +14:19:20 | 2 of 3 OK created view model DEMO_STG.v_stg_orders................... [SUCCESS 1 in 2.52s] +14:19:20 | 1 of 3 OK created view model DEMO_STG.v_stg_inventory................ [SUCCESS 1 in 2.59s] +14:19:20 | +14:19:20 | Finished running 3 view models in 7.98s. ``` \ No newline at end of file diff --git a/docs/t_links.md b/docs/t_links.md new file mode 100644 index 000000000..338f80036 --- /dev/null +++ b/docs/t_links.md @@ -0,0 +1,182 @@ +# Transactional Links + +Also known as non-historized or no-history links, transactional links record the transaction or 'event' components of +their referenced hub tables. They allow us to model the more granular relationships between entities. Some prime examples +are purchases, flights or emails; there is a record in the table for every event or transaction between the entities +instead of just one record per relation. + +Our transactional links will contain: + +1. A primary key. For t-links, we take the natural keys (prior to hashing) represented by the foreign key columns below and create a hash on a concatenation of them. +2. Foreign keys holding the primary key for each hub referenced in the link (2 or more depending on the number of hubs referenced) +3. A payload. The payload consists of concrete data for an entity, i.e. a transaction record. This could be +a transaction number, an amount paid, transaction type or more. The payload will contain all of the +concrete data for a transaction. +4. An effectivity date. Usually called ```EFFECTIVE_FROM```, this column is the business effective date of a +satellite record. It records that a record is valid from a specific point in time. In the case of a transaction, this +is usually the date on which the transaction occured. + +5. The load date or load date timestamp. +6. The source for the record + +!!! note + ```LOADDATE``` is the time the record is loaded into the database. ```EFFECTIVE_FROM``` is different and may hold a + different value, especially if there is a batch processing delay between when a business event happens and the + record arriving in the database for load. Having both dates allows us to ask the questions 'what did we know when' + and 'what happened when' using the ```LOADDATE``` and ```EFFECTIVE_FROM``` date accordingly. + +### Creating the model header + +Create a new dbt model as before. We'll call this one ```t_link_transactions```. + +The following header is what we use, but feel free to customise it to your needs: + +```t_link_transactions.sql``` +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='t_link') -}} +``` + +Transactional links are always incremental, as we load and add new records to the existing data set. + +[Read more about incremental models](https://docs.getdbt.com/v0.14.0/docs/configuring-incremental-models) + +### Adding the metadata + +Let's look at the metadata we need to provide to the [t_link_template](macros.md#t_link_template) macro. + +#### Source table + +The first piece of metadata we need is the source table. For transactional links this can sometimes be a little +trickier than other table types. We need particular columns to model the transaction or event which has occured in the +relationship between the hubs we are referencing, and therefore may need to create a staging layer specifically for the +purposes of feeding the transactional link. + +For this step, ensure you have the following columns present in the source table: + +1. A hashed transaction number as the primary key +2. Hashed foreign keys, one for each of the referenced hubs. +3. A payload. This will be data about the transaction itself e.g. the amount, type, date or non-hashed transaction number. +4. An ```EFFECTIVE_FROM``` date. This will usually be the date of the transaction. +5. A load date timestamp +6. A source + +Assuming you have a raw source table with these required columns, we can create a hashed staging table +using a dbt model, (let's call it ```stg_transactions_hashed.sql```) and use it for the source table +reference. dbt ensures dependencies are honoured when defining the source using a reference in this way. + +[Read more about the ref function](https://docs.getdbt.com/v0.14.0/docs/ref) + +```t_link_transactions.sql``` +```sql hl_lines="3" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='t_link') -}} + +{%- set source = [ref('stg_transactions_hashed')] -%} +``` + +!!! note + Make sure you surround the ref call with square brackets, as shown in the snippet + above. + + +#### Source columns + +Next, we define the columns which we would like to bring from the source. +We can use the columns we identified in the ```Source table``` section, above. + +```t_link_transactions.sql``` +```sql hl_lines="5 6 7 8 9 10" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='t_link') -}} + +{%- set source = [ref('stg_transactions_hashed')] -%} + +{%- set src_pk = 'TRANSACTION_PK' -%} +{%- set src_fk = ['CUSTOMER_FK', 'ORDER_FK'] -%} +{%- set src_payload = ['TRANSACTION_NUMBER', 'TRANSACTION_DATE', 'TYPE', 'AMOUNT'] -%} +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} +``` + +#### Target columns + +Now we can define the target column mapping. The [t_link_template](macros.md#t_link_template) does a lot of work for us if we +provide the metadata it requires. + +```t_link_transactions.sql``` +```sql hl_lines="12 13 14 15 16 17" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='t_link') -}} + +{%- set source = [ref('stg_transactions_hashed')] -%} + +{%- set src_pk = 'TRANSACTION_PK' -%} +{%- set src_fk = ['CUSTOMER_FK', 'ORDER_FK'] -%} +{%- set src_payload = ['TRANSACTION_NUMBER', 'TRANSACTION_DATE', 'TYPE', 'AMOUNT'] -%} +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_fk = source -%} +{%- set tgt_payload = source -%} +{%- set tgt_eff = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} +``` + +With these 6 additional lines, we have now informed the macro that we do not want to modify +our source data, we are simply using the ```source``` reference as shorthand for keeping the columns the same as +the source. In other tables in this walkthrough, notably [satellites](satellites.md#target-columns), we carried out +some manual mapping, but this isn't always necessary if we have all the columns we need in the staging layers. + +### Invoking the template + +Now we bring it all together and call the [t_link_template](macros.md#t_link_template) macro: + +```t_link_transactions.sql``` +```sql hl_lines="19 20 21" +{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='t_link') -}} + +{%- set source = [ref('stg_transactions_hashed')] -%} + +{%- set src_pk = 'TRANSACTION_PK' -%} +{%- set src_fk = ['CUSTOMER_FK', 'ORDER_FK'] -%} +{%- set src_payload = ['TRANSACTION_NUMBER', 'TRANSACTION_DATE', 'TYPE', 'AMOUNT'] -%} +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_fk = source -%} +{%- set tgt_payload = source -%} +{%- set tgt_eff = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, + source) }} +``` + +### Running dbt + +With our model complete, we can run dbt to create our ```t_link_transactions``` transactional link. + +```dbt run --models +t_link_transactions``` + +And our table will look like this: + +| TRANSACTION_PK | CUSTOMER_FK | ORDER_FK | TRANSACTION_NUMBER | TYPE | AMOUNT | EFFECTIVE_FROM | LOADDATE | SOURCE | +| --------------- | ----------- | --------- | ------------------ | ---- | ------- | -------------- | ----------- | ------ | +| BDEE76... | CA02D6... | CF97F1... | 123456789101 | CR | 100.00 | 1993-01-28 | 1993-01-29 | 2 | +| . | . | . | . | . | . | . | . | . | +| . | . | . | . | . | . | . | . | . | +| E0E7A8... | F67DF4... | 2C95D4... | 123456789104 | CR | 678.23 | 1993-01-28 | 1993-01-29 | 2 | + + +### Next steps + +We have now created a staging layer and a hub, link, satellite and transactional link. We'll be bringing new +table structures in future releases. + +Take a look at our [worked example](workedexample.md) for a demonstration of a realistic environment with pre-written +models for you to experiment with and learn from. \ No newline at end of file diff --git a/docs/walkthrough.md b/docs/walkthrough.md index d62d41b10..b7ae2bff6 100644 --- a/docs/walkthrough.md +++ b/docs/walkthrough.md @@ -21,7 +21,8 @@ We will: 2. A Snowflake account, trial or otherwise. [Sign up for a free 30-day trial here](https://trial.snowflake.com/ab/) -3. You must have downloaded and installed dbt, and [set up a project](https://docs.getdbt.com/docs/dbt-projects). +3. You must have downloaded and installed dbt 0.14(0.15 support will be added soon!), +and [set up a project](https://docs.getdbt.com/v0.14.0/docs/dbt-projects). 4. Sources should be set up in dbt [(see below)](#setting-up-sources). @@ -38,7 +39,7 @@ We will be using the ```source``` feature of dbt extensively throughout the docu data much easier, cleaner and more modular. We have provided an example below which shows a configuration similar to that used for the examples in our documentation, -however this feature is documented extensively in [the documentation for dbt](https://docs.getdbt.com/docs/using-sources). +however this feature is documented extensively in [the documentation for dbt](https://docs.getdbt.com/v0.14.0/docs/using-sources). After reading the above documentation, we recommend that you place the ```schema.yml``` file you create for your sources, in the root of your ```models``` folder, however you can place it where needed for your specific project and models. @@ -70,4 +71,4 @@ packages: And run ```dbt deps``` -[Read more on package installation (from dbt)](https://docs.getdbt.com/docs/package-management) \ No newline at end of file +[Read more on package installation (from dbt)](https://docs.getdbt.com/v0.14.0/docs/package-management) \ No newline at end of file diff --git a/docs/workedexample.md b/docs/workedexample.md index 1ee127306..292d07ef6 100644 --- a/docs/workedexample.md +++ b/docs/workedexample.md @@ -15,7 +15,7 @@ We will: - examine and profile the TPCH dataset to explore how we can map it to the Data Vault architecture. - create a raw staging layer. - process the raw staging layer. -- create a Data Vault with hubs, links and satellites using dbtvault and pre-written models. +- create a Data Vault with hubs, links, satellites and transactional links using dbtvault and pre-written models. ## Pre-requisites @@ -31,8 +31,9 @@ be the only necessary requirements you will need to get started with the example !!! warning We suggest a trial account so that you have full privileges and assurance that the demo is isolated from any - production warehouses. Whilst there shouldn't be any risk that the demo affects any unrelated data outside of the - scope of this project, you may use a corporate account or existing personal account at your own risk, + production warehouses. Whilst there is no risk that the demo affects any unrelated data outside of the + scope of this project, you will incur compute costs. + You may use a corporate account or existing personal account at your own risk. !!! note We have provided a complete ```requirements.txt``` to install with ```pip install -r requirements.txt``` @@ -41,20 +42,18 @@ be the only necessary requirements you will need to get started with the example ## Performance note -Please be aware that table structures are simulated from the TPCH-H dataset. The TPC-H dataset is a static view of data. +Please be aware that table structures are simulated from the TPC-H dataset. The TPC-H dataset is a static view of data. Only a subset of the data contains dates which allows us to simulate daily feeds. The ```v_stg_orders``` orders view is filtered by date, unfortunately the ```v_stg_inventory``` view cannot be filtered by date, so it ends up being a feed of the entire contents of the view each cycle. -This means that inventory related hubs links and satellites are populated once during the initial load cycle with +This means that inventory related hubs, links and satellites are populated once during the initial load cycle with everything and later cycles insert 0 new records in their left outer joins. As the dataset increases in size, e.g if you run with a larger TPC-H dataset (100, 1000 etc.) then be aware you are processing the entire inventory dataset each cycle, which results in unrepresentative load cycle times. -Unfortunately it's the nature of the dataset, it will not be that way for other datasets. We will look at additonal -datasets in the future! - -If you are feeling adventurous you may disable the inventory feed (```raw_inventory``` and child models) to see a more -accurate representation of performance. \ No newline at end of file +We have minimised the impact of this by adding a join in the raw inventory table on the raw orders table to ensure only +inventory items which are included in orders are fed into raw staging. The outcome is the same, but it significantly +optimises the loading process and thereby reduces load time. \ No newline at end of file diff --git a/macros/supporting/hash.sql b/macros/supporting/hash.sql index e78d0f353..a9130334d 100644 --- a/macros/supporting/hash.sql +++ b/macros/supporting/hash.sql @@ -14,22 +14,36 @@ -#} {%- macro hash(columns, alias, sort=false) -%} +{%- set hash = var('hash', 'MD5') -%} + +{#- Select hashing algorithm -#} +{%- if hash == 'MD5' -%} + {%- set hash_alg = 'MD5_BINARY' -%} + {%- set hash_size = 16 -%} +{%- elif hash == 'SHA' -%} + {%- set hash_alg = 'SHA2_BINARY' -%} + {%- set hash_size = 32 -%} +{%- else -%} + {%- set hash_alg = 'MD5_BINARY' -%} + {%- set hash_size = 32 -%} +{%- endif -%} + {#- Alpha sort columns before hashing -#} {%- if sort and columns is iterable and columns is not string -%} {%- set columns = columns|sort -%} {%- endif -%} {%- if columns is string %} - CAST(MD5_BINARY(UPPER(TRIM(CAST({{columns}} AS VARCHAR)))) AS BINARY(16)) AS {{alias}} + CAST({{- hash_alg -}}(UPPER(TRIM(CAST({{columns}} AS VARCHAR)))) AS BINARY({{- hash_size -}})) AS {{alias}} {%- else %} - CAST(MD5_BINARY(CONCAT( + CAST({{- hash_alg -}}(CONCAT( {%- for column in columns[:-1] %} - IFNULL(UPPER(TRIM(CAST({{column}} AS VARCHAR))), '^^'), '||', + IFNULL(UPPER(TRIM(CAST({{- column }} AS VARCHAR))), '^^'), '||', {%- if loop.last %} - IFNULL(UPPER(TRIM(CAST({{columns[-1]}} AS VARCHAR))), '^^') )) AS BINARY(16)) AS {{alias}} + IFNULL(UPPER(TRIM(CAST({{columns[-1]}} AS VARCHAR))), '^^') )) AS BINARY({{- hash_size -}})) AS {{alias}} {%- endif -%} {%- endfor -%} {%- endif -%} diff --git a/macros/tables/t_link_template.sql b/macros/tables/t_link_template.sql new file mode 100644 index 000000000..b8c1d49e3 --- /dev/null +++ b/macros/tables/t_link_template.sql @@ -0,0 +1,45 @@ +{#- Copyright 2019 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, + source) -%} + +{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_fk=src_fk, src_payload=src_payload, + src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, + tgt_pk=tgt_pk, tgt_fk=tgt_fk, tgt_payload=tgt_payload, + tgt_eff=tgt_eff, tgt_ldts=tgt_ldts, tgt_source=tgt_source, + source=source) -%} + +{%- set tgt_pk = tgt_cols['tgt_pk'] -%} +{%- set tgt_fk = tgt_cols['tgt_fk'] -%} +{%- set tgt_payload = tgt_cols['tgt_payload'] -%} +{%- set tgt_eff = tgt_cols['tgt_eff'] -%} +{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} +{%- set tgt_source = tgt_cols['tgt_source'] -%} + +{%- set is_union = dbtvault.is_union(source) -%} +-- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault +SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source], 'stg') }} +FROM ( + SELECT {{ dbtvault.prefix([src_pk, src_fk, src_payload, src_eff, + src_ldts, src_source], 'stg') }} + FROM {{ source[0] }} AS stg +) AS stg +{% if is_incremental() -%} +LEFT JOIN {{ this }} AS tgt +ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} +WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index c414690df..41a0f97da 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,6 +24,7 @@ nav: - Hubs: 'hubs.md' - Links: 'links.md' - Satellites: 'satellites.md' + - T-Links: 't_links.md' - Worked example: - Getting Started: 'workedexample.md' - Project setup: 'setup.md' From 13d9e5011bc4a2f1ab691c62011ff244c5fb4f3d Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 27 Nov 2019 15:47:46 +0000 Subject: [PATCH 091/157] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f28b699d..c3cd792b7 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ powered by [dbt](https://www.getdbt.com/), a registered trademark of [Fishtown A ## Worked example project -Get started quickly with our worked example: +Learn quickly with our worked example: - [Read the docs](https://dbtvault.readthedocs.io/en/latest/workedexample/) @@ -85,4 +85,4 @@ before anyone else! [View our contribution guidelines](CONTRIBUTING.md) ## License -[Apache 2.0](LICENSE.md) \ No newline at end of file +[Apache 2.0](LICENSE.md) From 5ce33f8015185300a954d34ca2873083e7d5c769 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 27 Nov 2019 18:39:18 +0000 Subject: [PATCH 092/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3cd792b7..628218e52 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ And run ## Usage -1. Create a model for your hub, link or satellite +1. Create a model for your table. 2. Provide metadata 3. Call the appropriate template macro From aaa313e1ad6e805ce0f8c0a59ec961d3f4be0e0e Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 29 Nov 2019 10:45:14 +0000 Subject: [PATCH 093/157] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f5c243eb7..92604ced1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: "[BUG] " -labels: '' +labels: bug assignees: DVAlexHiggs --- From 39da931fe121a8c0e445c39dee3a02740fa4684b Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 2 Dec 2019 01:04:53 +0000 Subject: [PATCH 094/157] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 628218e52..92dceb3fc 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,7 @@

-latest [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) - -stable [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4)](https://dbtvault.readthedocs.io/en/v0.4/?badge=v0.4) +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4)](https://dbtvault.readthedocs.io/en/v0.4/?badge=v0.4) [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) From 4bc1eda50dbb29dd300bb523c335e82e5e03a695 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 11 Dec 2019 14:27:47 +0000 Subject: [PATCH 095/157] Update README.md Added document link --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 92dceb3fc..7d9963c93 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

-

There will be a live demonstration of dbtvault at the next UK Data Vault User Group on Tuesday, December 3, 2019 @ 6pm in LONDON. - - Sign up for FREE now! +

Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. + + Download for FREE now!

From 9b23cea5b9bb55545d3a05a0247a209d55fcea67 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 11 Dec 2019 14:28:06 +0000 Subject: [PATCH 096/157] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 7d9963c93..551281cb7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@

-

Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. Download for FREE now! From d91c93068f1eac4ae14afd77b259e56c328396fd Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 11 Dec 2019 14:28:50 +0000 Subject: [PATCH 097/157] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 551281cb7..5152d43c2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@

+

News

Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. Download for FREE now! From cd1d3cebffe6ac21878a565fc009ffed7c1191a2 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 20 Dec 2019 13:47:30 +0000 Subject: [PATCH 098/157] Update README.md Added slack --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5152d43c2..b2af062f5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@

News

-

Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. + 1. We now have a slack channel, we'll be adding proper channels and github integration soon. + It's early days so bear with us! + 2. Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. Download for FREE now! -

@@ -12,6 +13,8 @@ [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4)](https://dbtvault.readthedocs.io/en/v0.4/?badge=v0.4) +[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) + [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) # dbtvault by [Datavault](https://www.data-vault.co.uk) From 4c01c4262fbc880cb40e733673877d12253be5ab Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 20 Dec 2019 14:32:28 +0000 Subject: [PATCH 099/157] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b2af062f5..4a035d19e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

News

- 1. We now have a slack channel, we'll be adding proper channels and github integration soon. - It's early days so bear with us! + 1. We now have a slack channel, use the button below to join :) + [![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) 2. Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. Download for FREE now! From e495a8650bd9d04d52470357b473fcb5d46a4516 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 20 Dec 2019 14:32:39 +0000 Subject: [PATCH 100/157] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 4a035d19e..2bc155f24 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@

News

1. We now have a slack channel, use the button below to join :) - [![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) 2. Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. Download for FREE now! From 9dc31da17832c09883d18719575f0333f3ae68f6 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 20 Dec 2019 14:34:44 +0000 Subject: [PATCH 101/157] Update README.md Fixed list --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2bc155f24..3ea66075a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@

News

- 1. We now have a slack channel, use the button below to join :) - 2. Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. - - Download for FREE now!

+ * We now have a slack channel, use the button below to join + * Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. + Download for FREE now! +

From 0f18a09b3f7e0c7db87d7cfc6b39aae146343d53 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 20 Dec 2019 15:08:12 +0000 Subject: [PATCH 102/157] Update README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3ea66075a..e7ec438ad 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,12 @@

-[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4)](https://dbtvault.readthedocs.io/en/v0.4/?badge=v0.4) -[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4)](https://dbtvault.readthedocs.io/en/v0.4/?badge=v0.4)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) + + + + [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) From 2ad93da395f4f7cce99658f51518eff7c1e890e0 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 8 Jan 2020 13:07:22 +0000 Subject: [PATCH 103/157] Version 0.4.1 Release - Added support for dbt v0.15 --- README.md | 9 ++------- dbt_project.yml | 3 ++- docs/bestpractices.md | 2 +- docs/changelog.md | 8 ++++++++ docs/hubs.md | 4 ++-- docs/links.md | 2 +- docs/macros.md | 2 +- docs/satellites.md | 4 ++-- docs/setup.md | 2 +- docs/t_links.md | 4 ++-- docs/walkthrough.md | 8 ++++---- 11 files changed, 26 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e7ec438ad..35a4b9de0 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,7 @@

-[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4)](https://dbtvault.readthedocs.io/en/v0.4/?badge=v0.4)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) - - - - +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4.1)](https://dbtvault.readthedocs.io/en/v0.4.1/?badge=v0.4.1)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) @@ -46,7 +42,6 @@ Learn quickly with our worked example: ## Installation -Ensure you are using dbt 0.14 (0.15 support will be added soon!) Add the following to your ```packages.yml``` @@ -54,7 +49,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.4 # Latest stable version + revision: v0.4.1 # Latest stable version ``` And run ```dbt deps``` diff --git a/dbt_project.yml b/dbt_project.yml index 8e00e9e5d..cb4660dfc 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,5 +1,6 @@ name: 'dbtvault' -version: '0.4' +version: '0.4.1' +require-dbt-version: [">=0.14.0", "<=0.15.0"] profile: 'dbtvault' diff --git a/docs/bestpractices.md b/docs/bestpractices.md index b898e91a0..eed5ffa69 100644 --- a/docs/bestpractices.md +++ b/docs/bestpractices.md @@ -122,4 +122,4 @@ models: It is possible to configure a hashing algorithm on a model-by-model basis using the hierarchical structure of the ```yaml``` file. We recommend you keep the hashing algorithm consistent across all tables, however, as per best practise. -Read the [dbt documentation](https://docs.getdbt.com/v0.14.0/docs/var) for further information on variable scoping. \ No newline at end of file +Read the [dbt documentation](https://docs.getdbt.com/v0.15.0/docs/var) for further information on variable scoping. \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 7ad437f88..ee15d75e6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.4.1] - 2020-01-08 +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4.1)](https://dbtvault.readthedocs.io/en/v0.3.3-pre/?badge=v0.3.3-pre) + +### Added + +- Support for dbt v0.15 + + ## [v0.4] - 2019-11-27 [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4)](https://dbtvault.readthedocs.io/en/v0.4-pre/?badge=v0.4) diff --git a/docs/hubs.md b/docs/hubs.md index 653f8dffc..d58c17ccb 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -26,7 +26,7 @@ The following header is what we use, but feel free to customise it to your needs Hubs are always incremental, as we load and add new records to the existing data set. -[Read more about incremental models](https://docs.getdbt.com/v0.14.0/docs/configuring-incremental-models) +[Read more about incremental models](https://docs.getdbt.com/v0.15.0/docs/configuring-incremental-models) !!! note "Dont worry!" The [hub_template](macros.md#hub_template) deals with the Data Vault @@ -42,7 +42,7 @@ The first piece of metadata we need is the source table. This step is easy, as i staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. dbt ensures dependencies are honoured when defining the source using a reference in this way. -[Read more about the ref function](https://docs.getdbt.com/v0.14.0/docs/ref) +[Read more about the ref function](https://docs.getdbt.com/v0.15.0/docs/ref) ```hub_customer.sql``` diff --git a/docs/links.md b/docs/links.md index 58a06b196..873e5b93a 100644 --- a/docs/links.md +++ b/docs/links.md @@ -38,7 +38,7 @@ The first piece of metadata we need is the source table. This step is easy, as w staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. dbt ensures dependencies are honoured when defining the source using a reference in this way. -[Read more about the ref function](https://docs.getdbt.com/v0.14.0/docs/ref) +[Read more about the ref function](https://docs.getdbt.com/v0.15.0/docs/ref) ```link_customer_nation.sql``` diff --git a/docs/macros.md b/docs/macros.md index 3000b13a5..73447e8fb 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -638,7 +638,7 @@ FROM MYDATABASE.MYSCHEMA.MYTABLE ``` !!! info - Sources need to be set up in dbt to ensure this works. [Read More](https://docs.getdbt.com/v0.14.0/docs/using-sources) + Sources need to be set up in dbt to ensure this works. [Read More](https://docs.getdbt.com/v0.15.0/docs/using-sources) #### Parameters diff --git a/docs/satellites.md b/docs/satellites.md index 69c651f2e..c64d9e1bb 100644 --- a/docs/satellites.md +++ b/docs/satellites.md @@ -42,7 +42,7 @@ The following header is what we use, but feel free to customise it to your needs Satellites are always incremental, as we load and add new records to the existing data set. -[Read more about incremental models](https://docs.getdbt.com/v0.14.0/docs/configuring-incremental-models) +[Read more about incremental models](https://docs.getdbt.com/v0.15.0/docs/configuring-incremental-models) ### Adding the metadata @@ -54,7 +54,7 @@ The first piece of metadata we need is the source table. This step is easy, as i staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. dbt ensures dependencies are honoured when defining the source using a reference in this way. -[Read more about the ref function](https://docs.getdbt.com/v0.14.0/docs/ref) +[Read more about the ref function](https://docs.getdbt.com/v0.15.0/docs/ref) ```sat_customer_details.sql``` ```sql hl_lines="3" diff --git a/docs/setup.md b/docs/setup.md index e104073cd..a4f3854fb 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -36,7 +36,7 @@ In your dbt profiles, you must create a connection with this name and provide th account details so that dbt can connect to your Snowflake databases. dbt provides their own documentation on how to configure profiles, so we suggest reading that -[here](https://docs.getdbt.com/v0.14.0/docs/configure-your-profile). +[here](https://docs.getdbt.com/v0.15.0/docs/configure-your-profile). A sample profile configuration is provided below which will get you started: diff --git a/docs/t_links.md b/docs/t_links.md index 338f80036..f52d5d61e 100644 --- a/docs/t_links.md +++ b/docs/t_links.md @@ -38,7 +38,7 @@ The following header is what we use, but feel free to customise it to your needs Transactional links are always incremental, as we load and add new records to the existing data set. -[Read more about incremental models](https://docs.getdbt.com/v0.14.0/docs/configuring-incremental-models) +[Read more about incremental models](https://docs.getdbt.com/v0.15.0/docs/configuring-incremental-models) ### Adding the metadata @@ -64,7 +64,7 @@ Assuming you have a raw source table with these required columns, we can create using a dbt model, (let's call it ```stg_transactions_hashed.sql```) and use it for the source table reference. dbt ensures dependencies are honoured when defining the source using a reference in this way. -[Read more about the ref function](https://docs.getdbt.com/v0.14.0/docs/ref) +[Read more about the ref function](https://docs.getdbt.com/v0.15.0/docs/ref) ```t_link_transactions.sql``` ```sql hl_lines="3" diff --git a/docs/walkthrough.md b/docs/walkthrough.md index b7ae2bff6..67ba332dd 100644 --- a/docs/walkthrough.md +++ b/docs/walkthrough.md @@ -21,8 +21,8 @@ We will: 2. A Snowflake account, trial or otherwise. [Sign up for a free 30-day trial here](https://trial.snowflake.com/ab/) -3. You must have downloaded and installed dbt 0.14(0.15 support will be added soon!), -and [set up a project](https://docs.getdbt.com/v0.14.0/docs/dbt-projects). +3. You must have downloaded and installed dbt 0.15, +and [set up a project](https://docs.getdbt.com/v0.15.0/docs/dbt-projects). 4. Sources should be set up in dbt [(see below)](#setting-up-sources). @@ -39,7 +39,7 @@ We will be using the ```source``` feature of dbt extensively throughout the docu data much easier, cleaner and more modular. We have provided an example below which shows a configuration similar to that used for the examples in our documentation, -however this feature is documented extensively in [the documentation for dbt](https://docs.getdbt.com/v0.14.0/docs/using-sources). +however this feature is documented extensively in [the documentation for dbt](https://docs.getdbt.com/v0.15.0/docs/using-sources). After reading the above documentation, we recommend that you place the ```schema.yml``` file you create for your sources, in the root of your ```models``` folder, however you can place it where needed for your specific project and models. @@ -71,4 +71,4 @@ packages: And run ```dbt deps``` -[Read more on package installation (from dbt)](https://docs.getdbt.com/v0.14.0/docs/package-management) \ No newline at end of file +[Read more on package installation (from dbt)](https://docs.getdbt.com/v0.15.0/docs/package-management) \ No newline at end of file From 713f040eabf58140f1b5f3dfbb38b35dd484e953 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 8 Jan 2020 13:12:39 +0000 Subject: [PATCH 104/157] Fixed 0.4.1 Link --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index ee15d75e6..667b191c8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [v0.4.1] - 2020-01-08 -[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4.1)](https://dbtvault.readthedocs.io/en/v0.3.3-pre/?badge=v0.3.3-pre) +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4.1)](https://dbtvault.readthedocs.io/en/v0.4.1/?badge=v0.4.1) ### Added From fb751901e50b0026767b61ab5d48949ece9f01d9 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 9 Jan 2020 11:09:31 +0000 Subject: [PATCH 105/157] Doc fix --- docs/macros.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/macros.md b/docs/macros.md index 73447e8fb..f8c0790e1 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -75,7 +75,7 @@ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, {{- config(...) -}} {%- set source = [ref('stg_customer_hashed')] -%} - . + {%- set src_pk = 'CUSTOMER_PK' -%} {%- set src_nk = 'CUSTOMER_ID' -%} {%- set src_ldts = 'LOADDATE' -%} From b11c5df78f7d53d4e234839805d60f287f89b100 Mon Sep 17 00:00:00 2001 From: Christopher Fisher Date: Mon, 24 Feb 2020 15:07:51 +0000 Subject: [PATCH 106/157] Version 0.5 release Metadata is now provided in the dbt_project.yml file. This means metadata can be managed in one place. ### Removed - Target column metadata mappings are no longer required. - Manual column mapping using triples to provide data-types and aliases (messy and bad practice). - Removed copyright notice from generated tables (we are open source, duh!) ### Fixed - Hashing a single column which contains a NULL value now works as intended (related to hash, multi_hash, staging macros). --- README.md | 1 + docs/bestpractices.md | 17 +- docs/changelog.md | 19 + docs/hubs.md | 166 ++-- docs/index.md | 9 +- docs/links.md | 172 +--- docs/loading.md | 10 +- docs/macros.md | 916 +++++++++++------- docs/metadata.md | 155 +++ docs/migrating.md | 55 ++ docs/roadmap.md | 7 +- docs/satellites.md | 154 +-- docs/setup.md | 71 +- docs/staging.md | 45 +- docs/t_links.md | 118 +-- docs/walkthrough.md | 12 +- macros/internal/check_relation.sql | 2 +- macros/internal/get_src_col_list.sql | 43 + macros/internal/is_multi_source.sql | 43 + macros/internal/is_union.sql | 2 +- macros/internal/new_union.sql | 37 + macros/internal/retrieve_tgt_cols.sql | 102 ++ macros/internal/single.sql | 3 +- macros/internal/source_columns.sql | 28 + macros/internal/validate_columns.sql | 2 +- macros/internal_deprecated/create_source.sql | 29 + .../internal_deprecated/create_tgt_cols.sql | 102 ++ macros/internal_deprecated/get_col_list.sql | 48 + macros/internal_deprecated/union.sql | 36 + macros/staging/add_columns.sql | 2 +- macros/staging/from.sql | 2 +- macros/staging/multi_hash.sql | 4 +- macros/supporting/cast.sql | 2 +- macros/supporting/hash.sql | 6 +- macros/supporting/prefix.sql | 2 +- macros/tables/hub.sql | 39 + macros/tables/link.sql | 39 + macros/tables/sat.sql | 43 + macros/tables/t_link.sql | 28 + macros/tables_deprecated/hub_template.sql | 50 + macros/tables_deprecated/link_template.sql | 50 + macros/tables_deprecated/sat_template.sql | 62 ++ macros/tables_deprecated/t_link_template.sql | 45 + mkdocs.yml | 16 +- 44 files changed, 1984 insertions(+), 810 deletions(-) create mode 100644 docs/metadata.md create mode 100644 docs/migrating.md create mode 100644 macros/internal/get_src_col_list.sql create mode 100644 macros/internal/is_multi_source.sql create mode 100644 macros/internal/new_union.sql create mode 100644 macros/internal/retrieve_tgt_cols.sql create mode 100644 macros/internal/source_columns.sql create mode 100644 macros/internal_deprecated/create_source.sql create mode 100644 macros/internal_deprecated/create_tgt_cols.sql create mode 100644 macros/internal_deprecated/get_col_list.sql create mode 100644 macros/internal_deprecated/union.sql create mode 100644 macros/tables/hub.sql create mode 100644 macros/tables/link.sql create mode 100644 macros/tables/sat.sql create mode 100644 macros/tables/t_link.sql create mode 100644 macros/tables_deprecated/hub_template.sql create mode 100644 macros/tables_deprecated/link_template.sql create mode 100644 macros/tables_deprecated/sat_template.sql create mode 100644 macros/tables_deprecated/t_link_template.sql diff --git a/README.md b/README.md index 35a4b9de0..1ad3f71ac 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ packages: - git: "https://github.com/Datavault-UK/dbtvault" revision: v0.4.1 # Latest stable version ``` + And run ```dbt deps``` diff --git a/docs/bestpractices.md b/docs/bestpractices.md index eed5ffa69..6fad0d0dd 100644 --- a/docs/bestpractices.md +++ b/docs/bestpractices.md @@ -17,7 +17,17 @@ For the next load you then can re-create the view with a different load date and manage a 'water-level' table which tracks the last load date for each source, and is incremented each load cycle. Do a join to the table to soft-select the next load date. -## Source +#### Staging columns after v0.5 + +With the removal of target column mappings the staging layer must include all columns which are required in the +raw vault. This means you will need to use the [add_columns](macros.md#add_columns) macro to add function, alias, and +constant based columns. You will also need to use the [multi_hash](macros.md#multi_hash) macro to add all hashed +columns. + +This is an opinionated design feature which dramatically simplifies the mapping of data into +the raw vault. This means that everything is derived from the staging layer. + +## Record source table code We suggest you use a code. This can be anything that makes sense for your particular context, though usually an integer or alpha-numeric value works well. The code is often used to look-up the full table name in a table. @@ -122,4 +132,7 @@ models: It is possible to configure a hashing algorithm on a model-by-model basis using the hierarchical structure of the ```yaml``` file. We recommend you keep the hashing algorithm consistent across all tables, however, as per best practise. -Read the [dbt documentation](https://docs.getdbt.com/v0.15.0/docs/var) for further information on variable scoping. \ No newline at end of file +Read the [dbt documentation](https://docs.getdbt.com/v0.15.0/docs/var) for further information on variable scoping. + +!!! warning + Stick with your chosen algorithm unless you can afford to full-refresh and you still have access to source data. \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 667b191c8..be5f3821b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.5] - 2020-02-24 +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.5)](https://dbtvault.readthedocs.io/en/v0.5/?badge=v0.5) + +### Added + +- Metadata is now provided in the ```dbt_project.yml``` file. This means metadata can be managed in one place. +Read [Migrating from v0.4](migrating.md) for more information. + +### Removed + +- Target column metadata mappings are no longer required. +- Manual column mapping using triples to provide data-types and aliases (messy and bad practice). +- Removed copyright notice from generated tables (we are open source, duh!) + +### Fixed + +- Hashing a single column which contains a ```NULL``` value now works as intended (related to: [hash](macros.md#hash), +[multi_hash](macros.md#multi_hash), [staging](macros.md#staging-macros)). + ## [v0.4.1] - 2020-01-08 [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4.1)](https://dbtvault.readthedocs.io/en/v0.4.1/?badge=v0.4.1) diff --git a/docs/hubs.md b/docs/hubs.md index d58c17ccb..f3efbb18c 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -20,8 +20,7 @@ The following header is what we use, but feel free to customise it to your needs ```hub_customer.sql``` ```sql -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} - +{{- config(materialized='incremental', schema='MYSCHEMA', tags='hub') -}} ``` Hubs are always incremental, as we load and add new records to the existing data set. @@ -29,116 +28,80 @@ Hubs are always incremental, as we load and add new records to the existing data [Read more about incremental models](https://docs.getdbt.com/v0.15.0/docs/configuring-incremental-models) !!! note "Dont worry!" - The [hub_template](macros.md#hub_template) deals with the Data Vault + The [hub](macros.md#hub) deals with the Data Vault 2.0 standards when loading into the hub from the source. We won't need to worry about unwanted duplicates. ### Adding the metadata -Let's look at the metadata we need to provide to the [hub_template](macros.md#hub_template) macro. +Let's look at the metadata we need to provide to the [hub](macros.md#hub) macro. -#### Source table +!!! tip "New in v0.5" + As of v0.5, metadata must be provided in the ```dbt_project.yml```. Please refer to our [metadata](metadata.md) page. -The first piece of metadata we need is the source table. This step is easy, as in this example we created the -staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. -dbt ensures dependencies are honoured when defining the source using a reference in this way. +!!! warning "hub_template deprecated" + For previous versions prior to v0.5, please use the [hub_template](macros.md#hub_template) macro. + -[Read more about the ref function](https://docs.getdbt.com/v0.15.0/docs/ref) +#### Source table -```hub_customer.sql``` +The first piece of metadata we need is the source table. This step is easy, as in this example we created the +staging layer ourselves. All we need to do is provide the name of stage table as a string in our metadata as follows. -```sql hl_lines="3" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} +```dbt_project.yml``` -{%- set source = [ref('stg_customer_hashed')] -%} +```yaml +hub_customer: + vars: + source: 'stg_customer_hashed' + ... ``` -!!! note - Make sure you surround the ref call with square brackets, as shown in the snippet - above. - #### Source columns Next, we define the columns which we would like to bring from the source. Using our knowledge of what columns we need in our ```hub_customer``` table, we can identify columns in our -staging layer which map to them: +staging layer which we will then use to form our hub: 1. A primary key, which is a hashed natural key. The ```CUSTOMER_PK``` we created earlier in the [staging](staging.md) section will be used for ```hub_customer```. -2. The natural key, ```CUSTOMER_ID``` which we added using the [add_columns](macros.md#add_columns) macro. +2. The natural key, ```CUSTOMER_KEY``` which we added using the [add_columns](macros.md#add_columns) macro. 3. A load date timestamp, which is present in the staging layer as ```LOADDATE``` 4. A ```SOURCE``` column. -We can now add this metadata to the model: - -```hub_customer.sql``` -```sql hl_lines="5 6 7 8" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_nk = 'CUSTOMER_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -``` - -#### Target columns +We can now add this metadata to the ```dbt_project.yml``` file: -Now we can define the target column mapping. The [hub_template](macros.md#hub_template) does a lot of work for us if we -provide the metadata it requires. +```dbt_project.yml``` -```hub_customer.sql``` -```sql hl_lines="10 11 12 13" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_nk = 'CUSTOMER_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_nk = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} +```yaml hl_lines="4 5 6 7" +hub_customer: + vars: + source: 'stg_customer_hashed' + src_pk: 'CUSTOMER_PK' + src_nk: 'CUSTOMER_KEY' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' ``` -With these 4 additional lines, we have provided our mapping from source to target. - -In this particular scenario we aren't renaming the columns or changing the data type, -so we have used the source reference as a shorthand for keeping the -same name and datatype as the source columns. If you want to rename columns or change their type, -this can be achieved by providing triples instead of the reference, -[see the documentation](macros.md#using-a-source-reference-for-the-target-metadata) -for more details. +!!! tip "New in v0.5" + Notice something missing? You no longer need to specify target columns in your metadata! All required columns + including constants, aliases, and functions must be handled using the [add_columns](macros.md#add_columns) macro + in the staging layer. ### Invoking the template -Now we bring it all together and call the [hub_template](macros.md#hub_template) macro: +Now all that is needed is to create your hub: ```hub_customer.sql``` -```sql hl_lines="15 16 17" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='hub') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_nk = 'CUSTOMER_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_nk = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} +```sql hl_lines="3 4" +{{- config(materialized='incremental', schema='MYSCHEMA', tags='hub') -}} + +{{ dbtvault.hub(var('src_pk'), var('src_nk'), var('src_ldts'), + var('src_source'), var('source')) }} ``` +Here we have added a call to the [hub](macros.md#hub) macro, referencing our variables declared in the +```dbt_project.yml``` file. + ### Running dbt With our model complete, we can run dbt to create our ```hub_customer``` hub. @@ -152,7 +115,7 @@ With our model complete, we can run dbt to create our ```hub_customer``` hub. And our table will look like this: -| CUSTOMER_PK | CUSTOMER_ID | LOADDATE | SOURCE | +| CUSTOMER_PK | CUSTOMER_KEY | LOADDATE | SOURCE | | ------------ | ------------ | ---------- | ------------ | | B8C37E... | 1001 | 1993-01-01 | 1 | | . | . | . | . | @@ -170,39 +133,30 @@ So, this data can and should be combined because these records have a shared key We can union the tables on that key, and create a hub containing a complete record set. We'll need to have a [staging model](staging.md) for each of the sources involved, -and provide them as a list of references to the source parameter as shown below. +and provide them as a list of strings in the ```dbt_project.yml``` file as shown below. !!! note If your primary key and natural key columns have different names across the different tables, they will need to be aliased to the same name in the respective staging layers via the [add_columns](macros.md#add_columns) macro. -This procedure only requires additional source references in the source list -metadata of our ```hub_customer``` model, the [hub_template](macros.md#hub_template) will handle the rest: - -```hub_customer.sql``` -```sql hl_lines="3 4 5" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags=['hub', 'union']) -}} - -{%- set source = [ref('stg_sap_customer_hashed'), - ref('stg_crm_customer_hashed'), - ref('stg_web_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_nk = 'CUSTOMER_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_nk = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.hub_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} +The union hub model will look exactly the same as creating a single source hub model. To create a union you need to +provide a list of sources rather than a single source in the metadata, the [hub](macros.md#hub) macro +will handle the rest. + +```dbt_project.yml``` +```yaml hl_lines="3 4 5" +hub_nation: + vars: + source: + - 'stg_customer_hashed' + - 'v_stg_inventory' + src_pk: 'NATION_PK' + src_nk: 'NATION_KEY' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' ``` ### Next steps -We have now created a staging layer and a hub. Next we will look at Links, which are created in a similar way. \ No newline at end of file +We have now created a staging layer and a hub. Next we will look at [links](links.md), which are created in a similar way. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 0ff9e049a..7a9c83b88 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,10 +17,10 @@ Our package runs inside the dbt environment, so you can use dbt to run other par dbtvault package for the Data Vault specific steps. !!! tip - #### Sign up for early-bird announcements + #### Sign up for early-bird announcements or join our Slack - [Sign up](https://www.data-vault.co.uk/dbtvault/) and get notified of new features and new releases - before anyone else! + [![Sign up](https://img.shields.io/badge/Email-Sign--up-blue)](https://www.data-vault.co.uk/dbtvault/) + [![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) ## What is Data Vault 2.0? Data Vault 2.0 is an Agile method that can be used to deliver a highly scalable enterprise Data Warehouse. @@ -43,7 +43,7 @@ The dbtvault package generates and runs Data Vault ETL code from your metadata. Just like other dbt projects, you write a model for each ETL step. You provide the metadata for each model as declared variables and include code to invoke a macro from the dbtvault package. -The macro does the rest of the work: it processes the metadata, generates Snowflake SQL and executes the load +The macro does the rest of the work: it processes the metadata, generates Snowflake SQL and then dbt executes the load respecting any and all dependencies. dbt even runs the load in parallel. As Data Vault 2.0 is designed for parallel load and Snowflake is highly performant, @@ -63,6 +63,7 @@ dbt works with the dbtvault package to: - Execute all generated SQL statements as a complete set. - Execute data load in parallel up to a user-defined number of parallel threads. - Generate data flow diagrams showing data lineage. +- Automatically build a documentation website. ## You Do Need Some Prior Knowledge About the Data Vault 2.0 Method If you are going to use the dbtvault package for your Data Vault 2.0 project, then we expect you to have some prior diff --git a/docs/links.md b/docs/links.md index 873e5b93a..53643831b 100644 --- a/docs/links.md +++ b/docs/links.md @@ -24,34 +24,29 @@ The following header is what we use, but feel free to customise it to your needs ```link_customer_nation.sql``` ```sql -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} +{{- config(materialized='incremental', schema='MYSCHEMA', tags='link') -}} ``` ### Adding the metadata -Now we need to provide some metadata to the [link_template](macros.md#link_template) macro. +Now we need to provide some metadata to the [link](macros.md#link) macro. #### Source table The first piece of metadata we need is the source table. This step is easy, as we created the -staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. -dbt ensures dependencies are honoured when defining the source using a reference in this way. +staging layer ourselves. All we need to do is provide the name of the staging layer in the ```dbt_project.yml``` file +and dbtvault will do the rest for us. -[Read more about the ref function](https://docs.getdbt.com/v0.15.0/docs/ref) +```dbt_project.yml``` -```link_customer_nation.sql``` - -```sql hl_lines="3" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} +```yaml +link_customer_nation: + vars: + source: 'stg_customer_hashed' + ... ``` -!!! note - Make sure you surround the ref call with square brackets, as shown in the snippet - above. - #### Source columns Next, we define the columns which we would like to bring from the source. @@ -60,95 +55,41 @@ staging layer which map to them: 1. A primary key, which is a combination of the two natural keys: In this case ```CUSTOMER_NATION_PK``` which we added in our staging layer. -2. ```CUSTOMER_ID``` which is one of our natural keys (we'll use the hashed column, ```CUSTOMER_PK```). -3. ```NATION_ID``` the second natural key (we'll use the hashed column, ```NATION_PK```). +2. ```CUSTOMER_KEY``` which is one of our natural keys (we'll use the hashed column, ```CUSTOMER_PK```). +3. ```NATION_KEY``` the second natural key (we'll use the hashed column, ```NATION_PK```). 4. A load date timestamp, which is present in the staging layer as ```LOADDATE``` 5. A ```SOURCE``` column. -We can now add this metadata to the model: - -```link_customer_nation.sql``` -```sql hl_lines="5 6 7 8" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - +We can now add this metadata to the ```dbt_project.yml``` file: + +```dbt_project.yml``` +```yaml hl_lines="4 5 6 7 8 9" +link_customer_nation: + vars: + source: 'stg_customer_hashed' + src_pk: 'LINK_CUSTOMER_NATION_PK' + src_fk: + - 'CUSTOMER_PK' + - 'NATION_PK' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' ``` !!! note We are using ```src_fk```, a list of the foreign keys. This is instead of the ```src_nk``` - we used when building the hubs. We must use square brackets when defining a list. - -#### Target columns - -Now we can define the target column mapping. The [link_template](macros.md#link_template) does a lot of work for us if we -provide the metadata it requires: - -```link_customer_nation.sql``` -```sql hl_lines="10 11 12 13 14 15" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} - -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -``` - -With these 4 additional lines, we have provided our mapping from source to target: - -- Observe that we are renaming the foreign key columns so that they have an ```FK``` suffix. -It's up to you: you could keep the ```PK``` suffix, use ```FK``` or something else. - -- For the rest of the ```tgt``` metadata, we do not wish to rename columns or change -any data types, so we are simply using the ```source``` reference as shorthand for keeping the columns the same as -the source. - -!!! info - There is nothing to stop you entering invalid type mappings in this step (i.e. trying to cast an invalid date format to a date), - so please ensure they are correct. - You will soon find out, however, as dbt will issue a warning to you. No harm done, but save time by providing - the correct metadata! + we used when building the hubs. These columns must be given in this list format in the ```dbt_project.yml``` file + for the links. ### Invoking the template -Now we bring it all together and call the [link_template](macros.md#link_template) macro: +Now we bring it all together and call the [link](macros.md#link_) macro: ```link_customer_nation.sql``` -```sql hl_lines="17 18 19" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='link') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} +```sql hl_lines="3 4" +{{- config(materialized='incremental', schema='MYSCHEMA', tags='link') -}} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} +{{ dbtvault.link(var('src_pk'), var('src_fk'), var('src_ldts'), + var('src_source'), var('source')) }} ``` @@ -178,43 +119,34 @@ So, this data can and should be combined because these records have a shared key We can union the tables on that key, and create a link containing a complete record set. We'll need to have a [staging model](staging.md) for each of the sources involved, -and provide them as a list of references to the source parameter as shown below. +and provide them as a list of strings in the ```dbt_project.yml``` file as shown below. !!! note If your primary key and natural key columns have different names across the different tables, they will need to be aliased to the same name in the respective staging layers via the [add_columns](macros.md#add_columns) macro. -This procedure only requires additional source references in the source list -metadata of our ```link_customer_nation``` model, the [link_template](macros.md#link_template) will handle the rest: - -```link_customer_nation.sql``` -```sql hl_lines="3 4 5" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags=['link', 'union']) -}} - -{%- set source = [ref('stg_sap_customer_hashed'), - ref('stg_crm_customer_hashed'), - ref('stg_web_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} - -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} +The union link model will look exactly the same as creating a single source link model. To create a union you need to +provide a list of sources rather than a single source in the metadata, the [link](macros.md#link) macro +will handle the rest. + +```dbt_project.yml``` +```yaml hl_lines="3 4 5" +link_nation_region: + vars: + source: + - 'stg_customer_hashed' + - 'v_stg_inventory' + src_pk: 'NATION_REGION_PK' + src_fk: + - 'NATION_PK' + - 'REGION_PK' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' ``` ### Next steps -We have now created a staging layer, a hub and a link. Next we will look at satellites. -These are a little more complicated, but don't worry, the [sat_template](macros.md#sat_template) will handle that for +We have now created a staging layer, a hub and a link. Next we will look at [satellites](satellites.md). +These are a little more complicated, but don't worry, the [sat](macros.md#sat) macro will handle that for us! diff --git a/docs/loading.md b/docs/loading.md index c297d3561..000ae774f 100644 --- a/docs/loading.md +++ b/docs/loading.md @@ -132,7 +132,7 @@ Now that we have loaded all records for the date ```1992-01-08```, we can increm Return to the ```dbt_project.yml``` file and change the date to ```1992-01-09```: ```dbt_project.yml``` -```yaml hl_lines="16" +```yaml hl_lines="24" models: snowflakeDemo: load: @@ -147,6 +147,14 @@ models: schema: "RAW" enabled: true materialized: incremental + hubs: + ... + links: + ... + sats: + ... + t_links: + ... vars: date: TO_DATE('1992-01-09') ``` diff --git a/docs/macros.md b/docs/macros.md index f8c0790e1..3e2befca4 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -1,55 +1,16 @@ -## Table templates +## Table templates ######(macros/tables) These macros form the core of the package and can be called in your models to build the different types of tables needed for your Data Vault. -### Metadata notes -#### Using a source reference for the target metadata +### hub -In the usage examples for the table template macros in this section, you will see ```source``` provided as the values -for some of the target metadata variables. ```source``` has been declared as a variable at the top of the models, -and holds a reference to the source table we are loading from. This is shorthand for retaining the name and data types -of the columns as they are provided in the ```src``` variables. You may wish to alias the columns or change their data -types in specific circumstances, which is possible by providing an additional parameter as a list of triples: -``` (source column name, data type to cast to, target column name)```. +Generates sql to build a hub table using the provided metadata in the ```dbt_project.yml```. -Both approaches are shown in the snippet below: - -```mysql -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- ...other src metadata... -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} -``` - -Here, we are keeping the ```tgt_pk``` (the target table's primary key) the same as the primary key identified in the -source (```src_pk```). -Behind the scenes, the macro will get the datatype of the column provided in the ```src_pk``` variable and generate a -mapping for us. If the ```src_pk``` column does not exist, an appropriate exception will be raised. - -Alternatively we have provided a manual mapping for the ```tgt_fk``` (the target table's foreign key). - -*For further details and examples on both methods, refer to the usage examples -and snippets in the table template documentation below (both Single-Source and Union).* - -!!! note - If only aliasing and **not** changing data types, we suggest using the [add_columns](#add_columns) macro. - - This aliasing approach is much simpler and processed in the staging layer instead. -___ - -### hub_template - -Generates sql to build a hub table using the provided metadata. - -```mysql -dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) +```jinja2 +{{ dbtvault.hub(var('src_pk'), var('src_nk'), var('src_ldts'), + var('src_source'), var('source')) }} ``` #### Parameters @@ -60,118 +21,69 @@ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, | src_nk | Source natural key column | String | String | check_circle | | src_ldts | Source loaddate timestamp column | String | String | check_circle | | src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | -| tgt_nk | Target natural key column | List/Reference | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | -| source | Staging model reference or table name | List | List | check_circle | +| source | Staging model reference or table name | String | List (YAML) | check_circle | #### Usage -``` yaml tab="Single-Source" - --- hub_customer.sql: - -{{- config(...) -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_nk = 'CUSTOMER_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_nk = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} -``` - -``` yaml tab="Union" - --- hub_parts.sql: +``` jinja2 {{- config(...) -}} -{%- set source = [ref('stg_parts_hashed'), - ref('stg_supplier_hashed'), - ref('stg_lineitem_hashed')] -%} - -{%- set src_pk = 'PART_PK' -%} -{%- set src_nk = 'PART_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_nk = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} +{{ dbtvault.hub(var('src_pk'), var('src_nk'), var('src_ldts'), + var('src_source'), var('source')) }} ``` +#### Example Output -#### Output - -```mysql tab="Single-Source" +```mysql tab='Single-Source' SELECT DISTINCT - CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, - CAST(stg.CUSTOMER_ID AS VARCHAR(38)) AS CUSTOMER_ID, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE + stg.CUSTOMER_PK, + stg.CUSTOMER_KEY, + stg.LOADDATE, + stg.SOURCE FROM ( - SELECT a.CUSTOMER_PK, a.CUSTOMER_ID, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_customer_hashed AS a + SELECT a.CUSTOMER_PK, a.CUSTOMER_KEY, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.v_stg_orders AS a ) AS stg LEFT JOIN MYDATABASE.MYSCHEMA.hub_customer AS tgt ON stg.CUSTOMER_PK = tgt.CUSTOMER_PK WHERE tgt.CUSTOMER_PK IS NULL ``` -```mysql tab="Union" +```mysql tab='Union' SELECT DISTINCT - CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, - CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE + stg.PART_PK, + stg.PART_KEY, + stg.LOADDATE, + stg.SOURCE FROM ( - SELECT src.PART_PK, src.PART_ID, src.LOADDATE, src.SOURCE, + SELECT src.PART_PK, src.PART_KEY, src.LOADDATE, src.SOURCE, LAG(SOURCE, 1) OVER(PARTITION by PART_PK ORDER BY PART_PK) AS FIRST_SOURCE FROM ( - SELECT a.PART_PK, a.PART_ID, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_parts_hashed AS a - UNION - SELECT b.PART_PK, b.PART_ID, b.LOADDATE, b.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_supplier_hashed AS b + SELECT a.PART_PK, a.PART_KEY, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.v_stg_orders AS a UNION - SELECT c.PART_PK, c.PART_ID, c.LOADDATE, c.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_lineitem_hashed AS c - ) as src + SELECT b.PART_PK, b.PART_KEY, b.LOADDATE, b.SOURCE + FROM MYDATABASE.MYSCHEMA.v_stg_inventory AS b + ) AS src ) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.hub_parts AS tgt +LEFT JOIN MYDATABASE.MYSCHEMA.hub_part AS tgt ON stg.PART_PK = tgt.PART_PK WHERE tgt.PART_PK IS NULL AND stg.FIRST_SOURCE IS NULL -``` +``` ___ -### link_template +### link -Generates sql to build a link table using the provided metadata. +Generates sql to build a link table using the provided metadata in the ```dbt_project.yml```. -```mysql -dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) +```jinja2 +{{ dbtvault.link(var('src_pk'), var('src_fk'), var('src_ldts'), + var('src_source'), var('source')) }} ``` #### Parameters @@ -179,128 +91,75 @@ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, | Parameter | Description | Type (Single-Source) | Type (Union) | Required? | | ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | | src_pk | Source primary key column | String | String | check_circle | -| src_fk | Source foreign key column(s) | List | List | check_circle | +| src_fk | Source foreign key column(s) | List (YAML) | List (YAML) | check_circle | | src_ldts | Source loaddate timestamp column | String | String | check_circle | | src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | -| tgt_fk | Target foreign key column | List/Reference | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | -| source | Staging model reference or table name | List | List | check_circle | +| source | Staging model reference or table name | String | List (YAML) | check_circle | #### Usage -``` yaml tab="Single-Source" +``` jinja2 --- link_customer_nation.sql: - {{- config(...) -}} -{%- set source = [ref('stg_crm_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} - -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} +{{ dbtvault.link(var('src_pk'), var('src_fk'), var('src_ldts'), + var('src_source'), var('source')) }} ``` -``` yaml tab="Union" - --- link_customer_nation_union.sql: - -{{- config(...) -}} - -{%- set source = [ref('stg_sap_customer_hashed'), - ref('stg_crm_customer_hashed'), - ref('stg_web_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} - -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} -``` - -#### Output +#### Example Output -```mysql tab="Single-Source" +```mysql tab='Single-Source' SELECT DISTINCT - CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, - CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, - CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE + stg.LINK_CUSTOMER_NATION_PK, + stg.CUSTOMER_PK, + stg.NATION_PK, + stg.LOADDATE, + stg.SOURCE FROM ( - SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS a + SELECT a.LINK_CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.v_stg_orders AS a ) AS stg LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation AS tgt -ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK -WHERE tgt.CUSTOMER_NATION_PK IS NULL +ON stg.LINK_CUSTOMER_NATION_PK = tgt.LINK_CUSTOMER_NATION_PK +WHERE tgt.LINK_CUSTOMER_NATION_PK IS NULL ``` -```mysql tab="Union" +```mysql tab='Union' SELECT DISTINCT - CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, - CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, - CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE + stg.NATION_REGION_PK, + stg.NATION_PK, + stg.REGION_PK, + stg.LOADDATE, + stg.SOURCE FROM ( - SELECT src.CUSTOMER_NATION_PK, src.CUSTOMER_PK, src.NATION_PK, src.LOADDATE, src.SOURCE, + SELECT src.NATION_REGION_PK, src.NATION_PK, src.REGION_PK, src.LOADDATE, src.SOURCE, LAG(SOURCE, 1) - OVER(PARTITION by CUSTOMER_NATION_PK - ORDER BY CUSTOMER_NATION_PK) AS FIRST_SOURCE + OVER(PARTITION by NATION_REGION_PK + ORDER BY NATION_REGION_PK) AS FIRST_SOURCE FROM ( - SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_sap_customer_hashed AS a - UNION - SELECT b.CUSTOMER_NATION_PK, b.CUSTOMER_PK, b.NATION_PK, b.LOADDATE, b.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS b + SELECT a.NATION_REGION_PK, a.NATION_PK, a.REGION_PK, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.v_stg_orders AS a UNION - SELECT c.CUSTOMER_NATION_PK, c.CUSTOMER_PK, c.NATION_PK, c.LOADDATE, c.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_web_customer_hashed AS c + SELECT b.NATION_REGION_PK, b.NATION_PK, b.REGION_PK, b.LOADDATE, b.SOURCE + FROM MYDATABASE.MYSCHEMA.v_stg_inventory AS b ) AS src ) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation_union AS tgt -ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK -WHERE tgt.CUSTOMER_NATION_PK IS NULL +LEFT JOIN MYDATABASE.MYSCHEMA.link_nation_region AS tgt +ON stg.NATION_REGION_PK = tgt.NATION_REGION_PK +WHERE tgt.NATION_REGION_PK IS NULL AND stg.FIRST_SOURCE IS NULL ``` ___ -### sat_template +### sat -Generates sql to build a satellite table using the provided metadata. +Generates sql to build a satellite table using the provided metadata in the ```dbt_project.yml```. -```mysql -dbtvault.sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - source) +```jinja2 +{{ dbtvault.sat(var('src_pk'), var('src_hashdiff'), var('src_payload'), + var('src_eff'), var('src_ldts'), var('src_source'), + var('source')) }} ``` #### Parameters @@ -309,100 +168,70 @@ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, | ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | | src_pk | Source primary key column | String | check_circle | | src_hashdiff | Source hashdiff column | String | check_circle | -| src_payload | Source payload column(s) | List | check_circle | +| src_payload | Source payload column(s) | List (YAML) | check_circle | | src_eff | Source effective from column | String | check_circle | | src_ldts | Source loaddate timestamp column | String | check_circle | | src_source | Name of the column containing the source ID | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | check_circle | -| tgt_hashdiff | Target hashdiff column | List/Reference | check_circle | -| tgt_payload | Target payload column | List/Reference | check_circle | -| tgt_eff | Target effective from column | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | -| source | Staging model reference or table name | List/Reference | check_circle | +| source | Staging model reference or table name | List (YAML) | check_circle | #### Usage -``` yaml - --- sat_customer_details.sql: +``` jinja2 {{- config(...) -}} -{%- set source = [ref('stg_customer_details_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} -{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} - -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} - -{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} - -{%- set tgt_payload = [[src_payload[0], 'VARCHAR(60)', 'NAME'], - [src_payload[1], 'DATE', 'DOB'], - [src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} - -{%- set tgt_eff = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - source) }} +{{ dbtvault.sat(var('src_pk'), var('src_hashdiff'), var('src_payload'), + var('src_eff'), var('src_ldts'), var('src_source'), + var('source')) }} ``` -#### Output +#### Example Output -```mysql +```mysql SELECT DISTINCT - CAST(e.CUSTOMER_HASHDIFF AS BINARY(16)) AS HASHDIFF, - CAST(e.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, - CAST(e.CUSTOMER_NAME AS VARCHAR(60)) AS NAME, - CAST(e.CUSTOMER_DOB AS DATE) AS DOB, - CAST(e.CUSTOMER_PHONE AS VARCHAR(15)) AS PHONE, - CAST(e.LOADDATE AS DATE) AS LOADDATE, - CAST(e.EFFECTIVE_FROM AS DATE) AS EFFECTIVE_FROM, - CAST(e.SOURCE AS VARCHAR(15)) AS SOURCE -FROM MYDATABASE.MYSCHEMA.stg_customer_details_hashed AS e + e.CUSTOMER_PK, + e.CUSTOMER_HASHDIFF, + e.NAME, + e.ADDRESS, + e.PHONE, + e.ACCBAL, + e.MKTSEGMENT, + e.COMMENT, + e.EFFECTIVE_FROM, + e.LOADDATE, + e.SOURCE +FROM MYDATABASE.MYSCHEMA.v_stg_orders AS e LEFT JOIN ( - SELECT d.CUSTOMER_PK, d.HASHDIFF, d.NAME, d.DOB, d.PHONE, d.EFFECTIVE_FROM, d.LOADDATE, d.SOURCE + SELECT d.CUSTOMER_PK, d.CUSTOMER_HASHDIFF, d.NAME, d.ADDRESS, d.PHONE, d.ACCBAL, d.MKTSEGMENT, d.COMMENT, d.EFFECTIVE_FROM, d.LOADDATE, d.SOURCE FROM ( - SELECT c.CUSTOMER_PK, c.HASHDIFF, c.NAME, c.DOB, c.PHONE, c.EFFECTIVE_FROM, c.LOADDATE, c.SOURCE, + SELECT c.CUSTOMER_PK, c.CUSTOMER_HASHDIFF, c.NAME, c.ADDRESS, c.PHONE, c.ACCBAL, c.MKTSEGMENT, c.COMMENT, c.EFFECTIVE_FROM, c.LOADDATE, c.SOURCE, CASE WHEN RANK() OVER (PARTITION BY c.CUSTOMER_PK ORDER BY c.LOADDATE DESC) = 1 THEN 'Y' ELSE 'N' END CURR_FLG FROM ( - SELECT a.CUSTOMER_PK, a.HASHDIFF, a.NAME, a.DOB, a.PHONE, a.EFFECTIVE_FROM, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.sat_customer_details as a - JOIN MYDATABASE.MYSCHEMA.stg_customer_details_hashed as b + SELECT a.CUSTOMER_PK, a.CUSTOMER_HASHDIFF, a.NAME, a.ADDRESS, a.PHONE, a.ACCBAL, a.MKTSEGMENT, a.COMMENT, a.EFFECTIVE_FROM, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.sat_order_customer_details as a + JOIN MYDATABASE.MYSCHEMA.v_stg_orders as b ON a.CUSTOMER_PK = b.CUSTOMER_PK ) as c ) AS d WHERE d.CURR_FLG = 'Y') AS src -ON src.HASHDIFF = e.CUSTOMER_HASHDIFF -WHERE src.HASHDIFF IS NULL +ON src.CUSTOMER_HASHDIFF = e.CUSTOMER_HASHDIFF +WHERE src.CUSTOMER_HASHDIFF IS NULL ``` - ___ -### t_link_template +### t_link -Generates sql to build a transactional link table using the provided metadata. +Generates sql to build a transactional link table using the provided metadata in the dbt_project.yml. -```mysql -dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, - source) +```jinja2 +{{ dbtvault.t_link(var('src_pk'), var('src_fk'), var('src_payload'), + var('src_eff'), var('src_ldts'), var('src_source'), + var('source')) }} ``` #### Parameters @@ -410,66 +239,42 @@ dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_sou | Parameter | Description | Type | Required? | | ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | | src_pk | Source primary key column | String | check_circle | -| src_fk | Source foreign key column(s) | List | check_circle | -| src_payload | Source payload column(s) | List | check_circle | +| src_fk | Source foreign key column(s) | List (YAML) | check_circle | +| src_payload | Source payload column(s) | List (YAML) | check_circle | | src_eff | Source effective from column | String | check_circle | | src_ldts | Source loaddate timestamp column | String | check_circle | | src_source | Name of the column containing the source ID | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | check_circle | -| tgt_fk | Target hashdiff column | List/Reference | check_circle | -| tgt_payload | Target foreign key column(s) | List/Reference | check_circle | -| tgt_eff | Target effective from column | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | -| source | Staging model reference or table name | List/Reference | check_circle | +| source | Staging model reference or table name | List (YAML) | check_circle | #### Usage -``` yaml +``` jinja2 --- t_link_transactions.sql: - -{{- config(...) -}} - -{%- set source = [ref('stg_transactions_hashed')] -%} - -{%- set src_pk = 'TRANSACTION_PK' -%} -{%- set src_fk = ['CUSTOMER_FK', 'ORDER_FK'] -%} -{%- set src_payload = ['TRANSACTION_NUMBER', 'TRANSACTION_DATE', 'TYPE', 'AMOUNT'] -%} -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = source -%} -{%- set tgt_payload = source -%} -{%- set tgt_eff = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} +{{- config(...) -}} -{{ dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, - source) }} +{{ dbtvault.t_link(var('src_pk'), var('src_fk'), var('src_payload'), + var('src_eff'), var('src_ldts'), var('src_source'), + var('source')) }} ``` -#### Output +#### Example Output -```mysql +```mysql SELECT DISTINCT - CAST(stg.TRANSACTION_PK AS BINARY) AS TRANSACTION_PK, - CAST(stg.CUSTOMER_FK AS BINARY) AS CUSTOMER_FK, - CAST(stg.ORDER_FK AS BINARY) AS ORDER_FK, - CAST(stg.TRANSACTION_NUMBER AS NUMBER(38,0)) AS TRANSACTION_NUMBER, - CAST(stg.TRANSACTION_DATE AS DATE) AS TRANSACTION_DATE, - CAST(stg.TYPE AS VARCHAR) AS TYPE, - CAST(stg.AMOUNT AS NUMBER(12,2)) AS AMOUNT, - CAST(stg.EFFECTIVE_FROM AS DATE) AS EFFECTIVE_FROM, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR) AS SOURCE + stg.TRANSACTION_PK, + stg.CUSTOMER_FK, + stg.ORDER_FK, + stg.TRANSACTION_NUMBER, + stg.TRANSACTION_DATE, + stg.TYPE, + stg.AMOUNT, + stg.EFFECTIVE_FROM, + stg.LOADDATE, + stg.SOURCE FROM ( SELECT stg.TRANSACTION_PK, stg.CUSTOMER_FK, stg.ORDER_FK, stg.TRANSACTION_NUMBER, stg.TRANSACTION_DATE, stg.TYPE, stg.AMOUNT, stg.EFFECTIVE_FROM, stg.LOADDATE, stg.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_transactions_hashed AS stg + FROM MYDATABASE.MYSCHEMA.v_stg_transactions AS stg ) AS stg LEFT JOIN MYDATABASE.MYSCHEMA.t_link_transactions AS tgt ON stg.TRANSACTION_PK = tgt.TRANSACTION_PK @@ -501,13 +306,13 @@ ___ This macro will generate SQL hashing sequences for one or more columns as below: ```sql tab='MD5' -CAST(MD5_BINARY(UPPER(TRIM(CAST(column1 AS VARCHAR)))) AS BINARY(16)) AS alias1, -CAST(MD5_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(16)) AS alias2 +CAST(MD5_BINARY(IFNULL((UPPER(TRIM(CAST(column1 AS VARCHAR)))), '^^')) AS BINARY(16)) AS alias1, +CAST(MD5_BINARY(IFNULL((UPPER(TRIM(CAST(column1 AS VARCHAR)))), '^^')) AS BINARY(16)) AS alias2 ``` ```sql tab='SHA' -CAST(SHA2_BINARY(UPPER(TRIM(CAST(column1 AS VARCHAR)))) AS BINARY(32)) AS alias1, -CAST(SHA2_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(32)) AS alias2 +CAST(SHA2_BINARY(IFNULL((UPPER(TRIM(CAST(column1 AS VARCHAR)))), '^^')) AS BINARY(32)) AS alias1, +CAST(SHA2_BINARY(IFNULL((UPPER(TRIM(CAST(column2 AS VARCHAR)))), '^^')) AS BINARY(32)) AS alias2 ``` #### Parameters @@ -531,7 +336,7 @@ CAST(SHA2_BINARY(UPPER(TRIM(CAST(column2 AS VARCHAR)))) AS BINARY(32)) AS alias2 #### Output ```mysql tab='MD5' -CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, +CAST(MD5_BINARY(IFNULL((UPPER(TRIM(CAST(column1 AS VARCHAR)))), '^^')) AS BINARY(16)) AS CUSTOMER_PK, CAST(MD5_BINARY(CONCAT( IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', @@ -541,7 +346,7 @@ CAST(MD5_BINARY(CONCAT( ``` ```mysql tab='SHA' -CAST(SHA2_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(32)) AS CUSTOMER_PK, +CAST(SHA2_BINARY(IFNULL((UPPER(TRIM(CAST(column1 AS VARCHAR)))), '^^')) AS BINARY(32)) AS CUSTOMER_PK, CAST(SHA2_BINARY(CONCAT( IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', @@ -558,6 +363,10 @@ ___ ### add_columns +!!! note + As of v0.5, column aliasing must be implemented using this macro. Manual type mappings in the raw vault are now + deprecated due to bad practice. + A simple macro for generating sequences of the following SQL: ```mysql column AS alias @@ -770,7 +579,7 @@ CAST(SHA2_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(32)) AS alias #### Output -```mysql tab = 'MD5' +```mysql tab='MD5' CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', @@ -823,8 +632,451 @@ a.CUSTOMERKEY ___ -## Internal +## Internal and Internal Deprecated ######(macros/internal) +######(macros/internal_deprecated) Internal macros support the other macros provided in this package. -They are used to process provided metadata and should not be called directly. \ No newline at end of file +They are used to process provided metadata and should not be called directly. + +## Table templates (deprecated) +######(macros/tables_deprecated) + +!!! warning "Deprecated" + The macros in this section are now deprecated as of v0.5, in favour of more streamlined metadata declaration and + usability. We have also removed raw vault column aliasing as this was bad practice. + +### hub_template + +Generates sql to build a hub table using the provided metadata. + +```mysql +dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) +``` + +#### Parameters + +| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | +| ------------- | --------------------------------------------------- | -------------------- | --------------- | ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | String | check_circle | +| src_nk | Source natural key column | String | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | +| tgt_nk | Target natural key column | List/Reference | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | +| source | Staging model reference or table name | List | List | check_circle | + +#### Usage + +``` yaml tab="Single-Source" + +-- hub_customer.sql: + +{{- config(...) -}} + +{%- set source = [ref('stg_customer_hashed')] -%} + . +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_nk = 'CUSTOMER_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_nk = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} +``` + +``` yaml tab="Union" + +-- hub_parts.sql: + +{{- config(...) -}} + +{%- set source = [ref('stg_parts_hashed'), + ref('stg_supplier_hashed'), + ref('stg_lineitem_hashed')] -%} + +{%- set src_pk = 'PART_PK' -%} +{%- set src_nk = 'PART_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_nk = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} +``` + + +#### Output + +```mysql tab="Single-Source" +SELECT DISTINCT + CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, + CAST(stg.CUSTOMER_ID AS VARCHAR(38)) AS CUSTOMER_ID, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT a.CUSTOMER_PK, a.CUSTOMER_ID, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_customer_hashed AS a +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.hub_customer AS tgt +ON stg.CUSTOMER_PK = tgt.CUSTOMER_PK +WHERE tgt.CUSTOMER_PK IS NULL +``` + +```mysql tab="Union" +SELECT DISTINCT + CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, + CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT src.PART_PK, src.PART_ID, src.LOADDATE, src.SOURCE, + LAG(SOURCE, 1) + OVER(PARTITION by PART_PK + ORDER BY PART_PK) AS FIRST_SOURCE + FROM ( + SELECT a.PART_PK, a.PART_ID, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_parts_hashed AS a + UNION + SELECT b.PART_PK, b.PART_ID, b.LOADDATE, b.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_supplier_hashed AS b + UNION + SELECT c.PART_PK, c.PART_ID, c.LOADDATE, c.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_lineitem_hashed AS c + ) as src +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.hub_parts AS tgt +ON stg.PART_PK = tgt.PART_PK +WHERE tgt.PART_PK IS NULL +AND stg.FIRST_SOURCE IS NULL +``` + +___ + +### link_template + +Generates sql to build a link table using the provided metadata. + +```mysql +dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) +``` + +#### Parameters + +| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | +| ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | String | check_circle | +| src_fk | Source foreign key column(s) | List | List | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | +| tgt_fk | Target foreign key column | List/Reference | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | +| source | Staging model reference or table name | List | List | check_circle | + +#### Usage + +``` yaml tab="Single-Source" + +-- link_customer_nation.sql: + +{{- config(...) -}} + +{%- set source = [ref('stg_crm_customer_hashed')] -%} + +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} +``` + +``` yaml tab="Union" + +-- link_customer_nation_union.sql: + +{{- config(...) -}} + +{%- set source = [ref('stg_sap_customer_hashed'), + ref('stg_crm_customer_hashed'), + ref('stg_web_customer_hashed')] -%} + +{%- set src_pk = 'CUSTOMER_NATION_PK' -%} +{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], + ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} + +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) }} +``` + +#### Output + +```mysql tab="Single-Source" +SELECT DISTINCT + CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, + CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, + CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS a +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation AS tgt +ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK +WHERE tgt.CUSTOMER_NATION_PK IS NULL +``` + +```mysql tab="Union" +SELECT DISTINCT + CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, + CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, + CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE +FROM ( + SELECT src.CUSTOMER_NATION_PK, src.CUSTOMER_PK, src.NATION_PK, src.LOADDATE, src.SOURCE, + LAG(SOURCE, 1) + OVER(PARTITION by CUSTOMER_NATION_PK + ORDER BY CUSTOMER_NATION_PK) AS FIRST_SOURCE + FROM ( + SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_sap_customer_hashed AS a + UNION + SELECT b.CUSTOMER_NATION_PK, b.CUSTOMER_PK, b.NATION_PK, b.LOADDATE, b.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS b + UNION + SELECT c.CUSTOMER_NATION_PK, c.CUSTOMER_PK, c.NATION_PK, c.LOADDATE, c.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_web_customer_hashed AS c + ) AS src +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation_union AS tgt +ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK +WHERE tgt.CUSTOMER_NATION_PK IS NULL +AND stg.FIRST_SOURCE IS NULL +``` + +___ + +### sat_template + +Generates sql to build a satellite table using the provided metadata. + +```mysql +dbtvault.sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source, + source) +``` + +#### Parameters + +| Parameter | Description | Type | Required? | +| ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | check_circle | +| src_hashdiff | Source hashdiff column | String | check_circle | +| src_payload | Source payload column(s) | List | check_circle | +| src_eff | Source effective from column | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | check_circle | +| tgt_hashdiff | Target hashdiff column | List/Reference | check_circle | +| tgt_payload | Target payload column | List/Reference | check_circle | +| tgt_eff | Target effective from column | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | +| source | Staging model reference or table name | List/Reference | check_circle | + +#### Usage + + +``` yaml + +-- sat_customer_details.sql: + +{{- config(...) -}} + +{%- set source = [ref('stg_customer_details_hashed')] -%} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} +{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} + +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} + +{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} + +{%- set tgt_payload = [[src_payload[0], 'VARCHAR(60)', 'NAME'], + [src_payload[1], 'DATE', 'DOB'], + [src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} + +{%- set tgt_eff = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source, + source) }} +``` + + +#### Output + +```mysql +SELECT DISTINCT + CAST(e.CUSTOMER_HASHDIFF AS BINARY(16)) AS HASHDIFF, + CAST(e.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, + CAST(e.CUSTOMER_NAME AS VARCHAR(60)) AS NAME, + CAST(e.CUSTOMER_DOB AS DATE) AS DOB, + CAST(e.CUSTOMER_PHONE AS VARCHAR(15)) AS PHONE, + CAST(e.LOADDATE AS DATE) AS LOADDATE, + CAST(e.EFFECTIVE_FROM AS DATE) AS EFFECTIVE_FROM, + CAST(e.SOURCE AS VARCHAR(15)) AS SOURCE +FROM MYDATABASE.MYSCHEMA.stg_customer_details_hashed AS e +LEFT JOIN ( + SELECT d.CUSTOMER_PK, d.HASHDIFF, d.NAME, d.DOB, d.PHONE, d.EFFECTIVE_FROM, d.LOADDATE, d.SOURCE + FROM ( + SELECT c.CUSTOMER_PK, c.HASHDIFF, c.NAME, c.DOB, c.PHONE, c.EFFECTIVE_FROM, c.LOADDATE, c.SOURCE, + CASE WHEN RANK() + OVER (PARTITION BY c.CUSTOMER_PK + ORDER BY c.LOADDATE DESC) = 1 + THEN 'Y' ELSE 'N' END CURR_FLG + FROM ( + SELECT a.CUSTOMER_PK, a.HASHDIFF, a.NAME, a.DOB, a.PHONE, a.EFFECTIVE_FROM, a.LOADDATE, a.SOURCE + FROM MYDATABASE.MYSCHEMA.sat_customer_details as a + JOIN MYDATABASE.MYSCHEMA.stg_customer_details_hashed as b + ON a.CUSTOMER_PK = b.CUSTOMER_PK + ) as c + ) AS d +WHERE d.CURR_FLG = 'Y') AS src +ON src.HASHDIFF = e.CUSTOMER_HASHDIFF +WHERE src.HASHDIFF IS NULL +``` + +___ + +### t_link_template + +Generates sql to build a transactional link table using the provided metadata. + +```mysql +dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, + source) +``` + +#### Parameters + +| Parameter | Description | Type | Required? | +| ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | check_circle | +| src_fk | Source foreign key column(s) | List | check_circle | +| src_payload | Source payload column(s) | List | check_circle | +| src_eff | Source effective from column | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | check_circle | +| tgt_fk | Target hashdiff column | List/Reference | check_circle | +| tgt_payload | Target foreign key column(s) | List/Reference | check_circle | +| tgt_eff | Target effective from column | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | +| source | Staging model reference or table name | List/Reference | check_circle | + +#### Usage + + +``` yaml + +-- t_link_transactions.sql: + +{{- config(...) -}} + +{%- set source = [ref('stg_transactions_hashed')] -%} + +{%- set src_pk = 'TRANSACTION_PK' -%} +{%- set src_fk = ['CUSTOMER_FK', 'ORDER_FK'] -%} +{%- set src_payload = ['TRANSACTION_NUMBER', 'TRANSACTION_DATE', 'TYPE', 'AMOUNT'] -%} +{%- set src_eff = 'EFFECTIVE_FROM' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_fk = source -%} +{%- set tgt_payload = source -%} +{%- set tgt_eff = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, + source) }} +``` + +#### Output + +```mysql +SELECT DISTINCT + CAST(stg.TRANSACTION_PK AS BINARY) AS TRANSACTION_PK, + CAST(stg.CUSTOMER_FK AS BINARY) AS CUSTOMER_FK, + CAST(stg.ORDER_FK AS BINARY) AS ORDER_FK, + CAST(stg.TRANSACTION_NUMBER AS NUMBER(38,0)) AS TRANSACTION_NUMBER, + CAST(stg.TRANSACTION_DATE AS DATE) AS TRANSACTION_DATE, + CAST(stg.TYPE AS VARCHAR) AS TYPE, + CAST(stg.AMOUNT AS NUMBER(12,2)) AS AMOUNT, + CAST(stg.EFFECTIVE_FROM AS DATE) AS EFFECTIVE_FROM, + CAST(stg.LOADDATE AS DATE) AS LOADDATE, + CAST(stg.SOURCE AS VARCHAR) AS SOURCE +FROM ( + SELECT stg.TRANSACTION_PK, stg.CUSTOMER_FK, stg.ORDER_FK, stg.TRANSACTION_NUMBER, stg.TRANSACTION_DATE, stg.TYPE, stg.AMOUNT, stg.EFFECTIVE_FROM, stg.LOADDATE, stg.SOURCE + FROM MYDATABASE.MYSCHEMA.stg_transactions_hashed AS stg +) AS stg +LEFT JOIN MYDATABASE.MYSCHEMA.t_link_transactions AS tgt +ON stg.TRANSACTION_PK = tgt.TRANSACTION_PK +WHERE tgt.TRANSACTION_PK IS NULL +``` +___ diff --git a/docs/metadata.md b/docs/metadata.md new file mode 100644 index 000000000..3f004517d --- /dev/null +++ b/docs/metadata.md @@ -0,0 +1,155 @@ +As of v0.5, metadata is provided to the models through the ```dbt_project.yml``` file instead of being specified in +the models themselves. This keeps the metadata all in one place and simplifies the use of dbtvault. + +!!! note + In v0.5, only source column metadata is necessary, we have removed target column metadata. + +#### Declaring sources + +Since v0.5, there is no longer the need to state the source using the ```ref``` macro, the new [macros](macros.md) do this all for +you. For single source models, just state the name of the source as a string. +For the case of union models, just state the sources as a list of strings. Examples of both of these can be seen below: + +```yaml tab="Single Source" +hub_customer: + vars: + source: 'v_stg_orders' +``` + +```yaml tab="Union" +hub_nation: + vars: + source: + - 'v_stg_orders' + - 'v_stg_inventory' +``` + +#### Hubs + +Only the source metadata is needed to build a hub, as column types and names are retained are retained in the target +table. The parameters that the [hub](macros.md#hub) macro accept are: + +| Parameter | Description | +| -------------| ---------------------------------------------------------| +| source | The staging table that feeds the hub. This can be a single source or a union. | +| src_pk | The column to use for the primary key (should be hashed) | +| src_nk | The natural key column that the primary key is based on. | +| src_ldts | The loaddate timestamp column of the record. | +| src_source | The source column of the record. | + +An example of the metadata structure for a hub is: + +```dbt_project.yml``` +```yaml +hub_customer: + vars: + source: 'stg_customer_hashed' + src_pk: 'CUSTOMER_PK' + src_nk: 'CUSTOMER_KEY' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' +``` + +#### Links + +The link metadata is very similar to the hub metadata. The parameters that the [link](macros.md#link) macro accept are: + +| Parameter | Description | +| -------------| ---------------------------------------------------------| +| source | The staging table that feeds the link. This can be single source or a union. | +| src_pk | The column to use for the primary key (should be hashed) | +| src_fk | The foreign key columns that the make up the primary link key. This must be entered as a list of strings. | +| src_ldts | The loaddate timestamp column of the record. | +| src_source | The source column of the record. | + +An example of the metadata structure for a link is: + +```dbt_project.yml``` +```yaml +link_customer_nation: + vars: + source: 'v_stg_orders' + src_pk: 'LINK_CUSTOMER_NATION_PK' + src_fk: + - 'CUSTOMER_PK' + - 'NATION_PK' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' +``` + +#### Satellites + +The metadata for satellites are different from that of links and hubs. The parameters the [sat](macros.md#sat) macro +accepts is: + +| Parameter | Description | +| -------------| ------------------------------------------------------------------- | +| source | The staging table that feeds the satellite (only single sources are used for satellites). | | +| src_pk | The primary key column of the table the satellite hangs off. | +| src_hashdiff | The hashdiff column of the satellite's payload. | +| src_payload | The columns that make up the payload of the satellite and are used in the hashdiff. The columns must be entered as a list of strings. | +| src_eff | The effective from date column. | +| src_ldts | The loaddate timestamp column of the record. | +| src_source | The source column of the record. | + +An example of the metadata structure for a satellite is: + +```dbt_project.yml``` +```yaml +sat_order_customer_details: + vars: + source: 'v_stg_orders' + src_pk: 'CUSTOMER_PK' + src_hashdiff: 'CUSTOMER_HASHDIFF' + src_payload: + - 'NAME' + - 'ADDRESS' + - 'PHONE' + - 'ACCBAL' + - 'MKTSEGMENT' + - 'COMMENT' + src_eff: 'EFFECTIVE_FROM' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' +``` + +#### Transactional links (non-historized links) + +The [t_link](macros.md#t_link) macro accepts the following parameters: + +| Parameter | Description | +| -------------| ------------------------------------------------------------------- | +| source | The staging table that feeds the transactional link (only single sources are used for transactional links). | +| src_pk | The primary key column of the transactional link. | +| src_fk | The foreign key columns that the make up the primary link key. This must be enter as a list of strings | +| src_payload | The columns that make up and payload of the transactional link. The columns must be entered as a list of strings. | +| src_eff | The effective from date column. | +| src_ldts | The loaddate timestamp column of the record. | +| src_source |The source column of the record. | + +```dbt_project.yml``` +```yaml +t_link_transactions: + vars: + source: 'v_stg_transactions' + src_pk: 'TRANSACTION_PK' + src_fk: + - 'CUSTOMER_FK' + - 'ORDER_FK' + src_payload: + - 'TRANSACTION_NUMBER' + - 'TRANSACTION_DATE' + - 'TYPE' + - 'AMOUNT' + src_eff: 'EFFECTIVE_FROM' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' +``` + +#### The problem with metadata + +As metadata is stored in the ```dbt_project.yml```, you can probably foresee the file getting very large for bigger +projects. To help manage large amounts of metadata, we recommend the use of external licence-based tools such as WhereScape, +Matillion, and erwin Data Modeller. We have future plans to improve metadata handling but in the meantime +any feedback or ideas are welcome. +___ \ No newline at end of file diff --git a/docs/migrating.md b/docs/migrating.md new file mode 100644 index 000000000..4200f3962 --- /dev/null +++ b/docs/migrating.md @@ -0,0 +1,55 @@ +# Migrating from v0.4 to v0.5 + +With the release of v0.5, we've moved metadata into vars in the ```dbt_project.yml``` file. Your old metadata would +have looked something like this: + +```sql +{{- config(materialized='incremental', schema='vlt', enabled=true, tags='hubs') -}} + +{%- set source = [ref('v_stg_orders')] -%} + +{%- set src_pk = 'CUSTOMER_PK' -%} +{%- set src_nk = 'CUSTOMER_ID' -%} +{%- set src_ldts = 'LOADDATE' -%} +{%- set src_source = 'SOURCE' -%} + +{%- set tgt_pk = source -%} +{%- set tgt_nk = source -%} +{%- set tgt_ldts = source -%} +{%- set tgt_source = source -%} + +{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) }} +``` + +With v0.5, several things have changed: + + - The metadata is now specified in the ```dbt_project.yml``` file. Below is how to structure this metadata in +the ```dbt_project.yml``` file. +- You can no longer specify target column mappings, your target table columns +will be derived from your source table metadata. + +The metadata is structured as follows in the ```dbt_project.yml``` file: + +```yaml +hub_customer: + vars: + source: 'v_stg_orders' + src_pk: 'CUSTOMER_PK' + src_nk: 'CUSTOMER_KEY' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' +``` + +The new example ```hub_customer.sql``` would then look like: + +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', tags='hub') -}} + +{{ dbtvault.hub(var('src_pk'), var('src_nk'), var('src_ldts'), + var('src_source'), var('source')) }} +``` + +!!! note + Please ensure that your ```dbt_project.yml``` file is formatted properly and contains the correct hierarchy. \ No newline at end of file diff --git a/docs/roadmap.md b/docs/roadmap.md index 25d14a095..a77eafccd 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -27,4 +27,9 @@ In future releases, we hope to include the following: - Bridge tables - Reference Tables - Mart loading helpers -- And more! \ No newline at end of file +- And more! + +### Additional features + +- Auditing +- Logging diff --git a/docs/satellites.md b/docs/satellites.md index c64d9e1bb..cd3a393c4 100644 --- a/docs/satellites.md +++ b/docs/satellites.md @@ -37,7 +37,7 @@ The following header is what we use, but feel free to customise it to your needs ```sat_customer_details.sql``` ```sql -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} +{{- config(materialized='incremental', schema='MYSCHEMA', tags='sat') -}} ``` Satellites are always incremental, as we load and add new records to the existing data set. @@ -46,28 +46,21 @@ Satellites are always incremental, as we load and add new records to the existin ### Adding the metadata -Let's look at the metadata we need to provide to the [sat_template](macros.md#sat_template) macro. +Let's look at the metadata we need to provide to the [sat](macros.md#sat) macro via the ```dbt_project.yml``` file. #### Source table The first piece of metadata we need is the source table. This step is easy, as in this example we created the -staging layer ourselves. All we need to do is provide a reference to the model we created, and dbt will do the rest for us. -dbt ensures dependencies are honoured when defining the source using a reference in this way. - -[Read more about the ref function](https://docs.getdbt.com/v0.15.0/docs/ref) - -```sat_customer_details.sql``` -```sql hl_lines="3" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} +staging layer ourselves. All we need to do is provide the name of stage table as a string in our metadata +as follows. + +```dbt_project.yml``` +```yaml +sat_customer_details: + vars: + source: 'stg_customer_hashed' ``` - -!!! note - Make sure you surround the ref call with square brackets, as shown in the snippet - above. - #### Source columns Next, we define the columns which we would like to bring from the source. @@ -83,104 +76,35 @@ raw staging layer via an [add_columns](macros.md#add_columns) macro call. 5. A load date timestamp, which is present in the staging layer as ```LOADDATE```. 6. A ```SOURCE``` column. -We can now add this metadata to the model: - -```sat_customer_details.sql``` -```sql hl_lines="5 6 7 9 10 11" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} -{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} - -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} -``` - -#### Target columns - -Now we can define the target column mapping. The [sat_template](macros.md#sat_template) does a lot of work for us if we -provide the metadata it requires. We can define which source columns map to the required target columns and -define a column type at the same time: - -```sat_customer_details.sql``` -```sql hl_lines="13 14 15 16 17 19 20 21" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} -{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} - -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_hashdiff = [src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} -{%- set tgt_payload = [[src_payload[0], 'VARCHAR(60)', 'NAME'], - [src_payload[1], 'DATE', 'DOB'], - [src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} - -{%- set tgt_eff = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} +We can now add this metadata to the ```dbt_project```: + +```dbt_project.yml``` +```yaml hl_lines="4 5 6 7 8 9 10 11 12" +sat_order_customer_details: + vars: + source: 'stg_customer_hashed' + src_pk: 'CUSTOMER_PK' + src_hashdiff: 'CUSTOMER_HASHDIFF' + src_payload: + - 'CUSTOMER_NAME' + - 'CUSTOMER_DOB' + - 'CUSTOMER_PHONE' + src_eff: 'EFFECTIVE_FROM' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' ``` -With these 6 additional lines, we have now informed the macro how to transform our source data: - -- We have provided our mapping from source to target. We're renaming the payload columns and the hashdiff here. -We are removing the ```CUSTOMER``` prefix, as this satellite is specifically for customer details and it's -superfluous. Renaming will always depend on your specific project and context, however. - -- For the rest of the ```tgt``` metadata, we do not wish to rename columns or change -any data types, so we are simply using the ```source``` reference as shorthand for keeping the columns the same as -the source. - -!!! info - There is nothing to stop you entering invalid type mappings in this step (i.e. trying to cast an invalid date format to a date), - so please ensure they are correct. - You will soon find out, however, as dbt will issue a warning to you. No harm done, but save time by providing - accurate metadata! - ### Invoking the template -Now we bring it all together and call the [sat_template](macros.md#sat_template) macro: +Now we bring it all together and call the [sat](macros.md#sat_) macro: ```sat_customer_details.sql``` -```sql hl_lines="23 24 25 26 27" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='sat') -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} -{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} - -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} -{%- set tgt_payload = [[ src_payload[0], 'VARCHAR(60)', 'NAME'], - [ src_payload[1], 'DATE', 'DOB'], - [ src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} - -{%- set tgt_eff = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - source) }} +```sql hl_lines="3 4 5" +{{- config(materialized='incremental', schema='MYSCHEMA', tags='satellite') -}} +{{ dbtvault.sat(var('src_pk'), var('src_hashdiff'), var('src_payload'), + var('src_eff'), var('src_ldts'), var('src_source'), + var('source')) }} ``` ### Running dbt @@ -191,14 +115,14 @@ With our model complete, we can run dbt to create our ```sat_customer_details``` And our table will look like this: -| CUSTOMER_PK | HASHDIFF | NAME | DOB | PHONE | EFFECTIVE_FROM | LOADDATE | SOURCE | -| ------------ | ------------ | ---------- | ------------ | --------------- | -------------- | ----------- | ------ | -| B8C37E... | 3C5984... | Alice | 1997-04-24 | 17-214-233-1214 | 1993-01-01 | 1993-01-01 | 1 | -| . | . | . | . | . | . | . | 1 | -| . | . | . | . | . | . | . | 1 | -| FED333... | D8CB1F... | Dom | 2018-04-13 | 17-214-233-1217 | 1993-01-01 | 1993-01-01 | 1 | +| CUSTOMER_PK | CUSTOMER_HASHDIFF | CUSTOMER_NAME | CUSTOMER_DOB | CUSTOMER_PHONE | EFFECTIVE_FROM | LOADDATE | SOURCE | +| ------------ | ------------ | ---------- | ------------ | --------------- | -------------- | ----------- | ------ | +| B8C37E... | 3C5984... | Alice | 1997-04-24 | 17-214-233-1214 | 1993-01-01 | 1993-01-01 | 1 | +| . | . | . | . | . | . | . | 1 | +| . | . | . | . | . | . | . | 1 | +| FED333... | D8CB1F... | Dom | 2018-04-13 | 17-214-233-1217 | 1993-01-01 | 1993-01-01 | 1 | ### Next steps -We have now created a staging layer and a hub, link and satellite. Next we will ook at transactional links. \ No newline at end of file +We have now created a staging layer and a hub, link and satellite. Next we will look at [transactional links](t_links.md). \ No newline at end of file diff --git a/docs/setup.md b/docs/setup.md index a4f3854fb..d6518e70d 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -85,8 +85,8 @@ reasonable time-frame, however, you may run with as many threads as required. ## The project file -The ```dbt_project.yml``` file provided with the project is mostly standard. The main additions are the -settings for the models and the ```vars```. +As of v0.5, the ```dbt_project.yml``` file is now used as a metadata store. Below is an example file showing the +metadata for a single instance of each of the current table types. ```dbt_project.yml``` ```yaml @@ -105,6 +105,64 @@ models: schema: "RAW" enabled: true materialized: incremental + hubs: + enabled: true + hub_customer: + vars: + source: 'v_stg_orders' + src_pk: 'CUSTOMER_PK' + src_nk: 'CUSTOMER_KEY' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' + ... + links: + enabled: true + link_customer_nation: + vars: + source: 'v_stg_orders' + src_pk: 'LINK_CUSTOMER_NATION_PK' + src_fk: + - 'CUSTOMER_PK' + - 'NATION_PK' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' + ... + sats: + enabled: true + sat_order_customer_details: + vars: + source: 'v_stg_orders' + src_pk: 'CUSTOMER_PK' + src_hashdiff: 'CUSTOMER_HASHDIFF' + src_payload: + - 'NAME' + - 'ADDRESS' + - 'PHONE' + - 'ACCBAL' + - 'MKTSEGMENT' + - 'COMMENT' + src_eff: 'EFFECTIVE_FROM' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' + ... + t_links: + enabled: true + t_link_transactions: + vars: + source: 'v_stg_transactions' + src_pk: 'TRANSACTION_PK' + src_fk: + - 'CUSTOMER_FK' + - 'ORDER_FK' + src_payload: + - 'TRANSACTION_NUMBER' + - 'TRANSACTION_DATE' + - 'TYPE' + - 'AMOUNT' + src_eff: 'EFFECTIVE_FROM' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' + ... vars: date: TO_DATE('1992-01-08') ``` @@ -117,7 +175,14 @@ schema, and models in the sub-directories ```stage``` and ```source``` should ha as their materialization. Many of these attributes are also provided in the files themselves and take precedence over these settings anyway, this is just a design choice. -#### vars +#### table metadata + +The table metadata is now provided, as of v0.5, in the ```dbt_project.yml``` file as seen in the above example. +For each of your table models you must specify the metadata using the correct hierarchy. The metadata provided here is +for the ```hub_customer.sql```, ```link_customer_nation.sql```, and ```sat_order_customer_details.sql``` models. + +#### global vars +On line 73, we have vars that will apply to all models. To simulate day-feeds, we use a variable we have named ```date``` which is used in the ```SRC``` models to load for a specific date. This is described in more detail in the [Profiling TPC-H](sourceprofile.md) section. \ No newline at end of file diff --git a/docs/staging.md b/docs/staging.md index 8e18e9c6a..9115dabe7 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -3,6 +3,8 @@ The dbtvault package assumes you've already loaded a Snowflake database staging table with raw data from a source system or feed (the 'raw staging layer'). +### Pre-conditions + There are a few conditions that need to be met for the dbtvault package to work: - All records are for the same ```load_datetime``` @@ -11,6 +13,8 @@ There are a few conditions that need to be met for the dbtvault package to work: Instead of truncating and loading, you may also build a view over the table to filter out the right records and load from the view. +### Let's Begin + The raw staging table needs to be pre-processed to add extra columns of data to make it ready to load to the raw vault. Specifically, we need to add primary key hashes, hashdiffs, and any implied fixed-value columns (see the diagram). @@ -21,7 +25,7 @@ We also need to ensure column names align with target hub or link tables. We've implemented hashing as the only option for now, though a non-hashed version will be added in future releases. -## Creating the model +## Creating the stage model To prepare our raw staging layer for loading the vault, we create a dbt model and call dbtvault staging macros with provided metadata. @@ -93,10 +97,10 @@ After adding the macro call, our model will now look something like this: {%- set source_table = source('MYSOURCE', 'stg_customer') -%} -{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), - ('NATION_ID', 'NATION_PK'), - (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), - (['CUSTOMER_ID', 'CUSTOMER_NAME', +{{ dbtvault.multi_hash([('CUSTOMER_KEY', 'CUSTOMER_PK'), + ('NATION_KEY', 'NATION_PK'), + (['CUSTOMER_KEY', 'NATION_KEY'], 'CUSTOMER_NATION_PK'), + (['CUSTOMER_KEY', 'CUSTOMER_NAME', 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], 'CUSTOMER_HASHDIFF', true)]) -}}, ``` @@ -106,13 +110,13 @@ After adding the macro call, our model will now look something like this: This call will: -- Hash the ```CUSTOMER_ID``` column, and create a new column called ```CUSTOMER_PK``` containing the hash +- Hash the ```CUSTOMER_KEY``` column, and create a new column called ```CUSTOMER_PK``` containing the hash value. -- Hash the ```NATION_ID``` column, and create a new column called ```NATION_PK``` containing the hash +- Hash the ```NATION_KEY``` column, and create a new column called ```NATION_PK``` containing the hash value. -- Concatenate the values in the ```CUSTOMER_ID``` and ```NATION_ID``` columns and hash them in the order supplied, creating a new +- Concatenate the values in the ```CUSTOMER_KEY``` and ```NATION_KEY``` columns and hash them in the order supplied, creating a new column called ```CUSTOMER_NATION_PK``` containing the hash of the combination of the values. -- Concatenate the values in the ```CUSTOMER_ID```, ```CUSTOMER_NAME```, ```CUSTOMER_PHONE```, ```CUSTOMER_DOB``` +- Concatenate the values in the ```CUSTOMER_KEY```, ```CUSTOMER_NAME```, ```CUSTOMER_PHONE```, ```CUSTOMER_DOB``` columns and hash them, creating a new column called ```CUSTOMER_NATION_PK``` containing the hash of the combination of the values. The ```true``` parameter should be provided so that the columns are alpha-sorted. @@ -127,15 +131,14 @@ We now add the column names we want to bring forward/feed from the raw staging t To include all columns which exist in the source table, we provide the ```source_table``` variable we created earlier. We will also need to add some additional columns to our staging layer, containing 'constants' implied by the context of the -staging data. For example, we may add a source table code value, or the the load date, or some other constant needed in +staging data. For example, we can add a source table code value for audit purposes, the load date, or some other constant needed in the primary key. We can also override any columns coming in from the source, with different data. We may want to do this if a source column already exists in the raw stage and the values aren't appropriate. -We provide the constant by adding an ```!``` to the data and alias them with the same name as the column we want to -override. You will have another opportunity to rename these columns, as well as cast them to different data types -later when creating the raw vault tables. We can also use this method to create any new columns which do not already +We provide a constant by adding an ```!``` to the data and alias them with the same name as the column we want to +override. We can also use this method to create any new columns which do not already exist in the source. @@ -146,10 +149,10 @@ exist in the source. {%- set source_table = source('MYSOURCE', 'stg_customer') -%} -{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), - ('NATION_ID', 'NATION_PK'), - (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), - (['CUSTOMER_ID', 'CUSTOMER_NAME', +{{ dbtvault.multi_hash([('CUSTOMER_KEY', 'CUSTOMER_PK'), + ('NATION_KEY', 'NATION_PK'), + (['CUSTOMER_KEY', 'NATION_KEY'], 'CUSTOMER_NATION_PK'), + (['CUSTOMER_KEY', 'CUSTOMER_NAME', 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], 'CUSTOMER_HASHDIFF', true)]) -}}, @@ -182,10 +185,10 @@ After adding the footer, our completed model should now look like this: {%- set source_table = source('MYSOURCE', 'stg_customer') -%} -{{ dbtvault.multi_hash([('CUSTOMER_ID', 'CUSTOMER_PK'), - ('NATION_ID', 'NATION_PK'), - (['CUSTOMER_ID', 'NATION_ID'], 'CUSTOMER_NATION_PK'), - (['CUSTOMER_ID', 'CUSTOMER_NAME', +{{ dbtvault.multi_hash([('CUSTOMER_KEY', 'CUSTOMER_PK'), + ('NATION_KEY', 'NATION_PK'), + (['CUSTOMER_KEY', 'NATION_KEY'], 'CUSTOMER_NATION_PK'), + (['CUSTOMER_KEY', 'CUSTOMER_NAME', 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], 'CUSTOMER_HASHDIFF', true)]) -}}, diff --git a/docs/t_links.md b/docs/t_links.md index f52d5d61e..4c365e4e5 100644 --- a/docs/t_links.md +++ b/docs/t_links.md @@ -33,7 +33,7 @@ The following header is what we use, but feel free to customise it to your needs ```t_link_transactions.sql``` ```sql -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='t_link') -}} +{{- config(materialized='incremental', schema='MYSCHEMA', tags='t_link') -}} ``` Transactional links are always incremental, as we load and add new records to the existing data set. @@ -42,7 +42,7 @@ Transactional links are always incremental, as we load and add new records to th ### Adding the metadata -Let's look at the metadata we need to provide to the [t_link_template](macros.md#t_link_template) macro. +Let's look at the metadata we need to provide to the [t_link](macros.md#t_link) macro. #### Source table @@ -61,100 +61,52 @@ For this step, ensure you have the following columns present in the source table 6. A source Assuming you have a raw source table with these required columns, we can create a hashed staging table -using a dbt model, (let's call it ```stg_transactions_hashed.sql```) and use it for the source table -reference. dbt ensures dependencies are honoured when defining the source using a reference in this way. +using a dbt model, (let's call it ```stg_transactions_hashed.sql```) and this is the table we reference in the +```dbt_project.yml``` file as a string. -[Read more about the ref function](https://docs.getdbt.com/v0.15.0/docs/ref) - -```t_link_transactions.sql``` -```sql hl_lines="3" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='t_link') -}} - -{%- set source = [ref('stg_transactions_hashed')] -%} -``` - -!!! note - Make sure you surround the ref call with square brackets, as shown in the snippet - above. - +```dbt_project.yml``` +```yaml +t_link_transactions: + vars: + source: 'stg_transactions_hashed' + ... +``` #### Source columns Next, we define the columns which we would like to bring from the source. We can use the columns we identified in the ```Source table``` section, above. -```t_link_transactions.sql``` -```sql hl_lines="5 6 7 8 9 10" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='t_link') -}} - -{%- set source = [ref('stg_transactions_hashed')] -%} - -{%- set src_pk = 'TRANSACTION_PK' -%} -{%- set src_fk = ['CUSTOMER_FK', 'ORDER_FK'] -%} -{%- set src_payload = ['TRANSACTION_NUMBER', 'TRANSACTION_DATE', 'TYPE', 'AMOUNT'] -%} -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} +```dbt_project.yml``` +```yaml hl_lines="4 5 6 7 8 9 10 11 12 13 14 15" +t_link_transactions: + vars: + source: 'stg_transactions_hashed' + src_pk: 'TRANSACTION_PK' + src_fk: + - 'CUSTOMER_FK' + - 'ORDER_FK' + src_payload: + - 'TRANSACTION_NUMBER' + - 'TRANSACTION_DATE' + - 'TYPE' + - 'AMOUNT' + src_eff: 'EFFECTIVE_FROM' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' ``` -#### Target columns - -Now we can define the target column mapping. The [t_link_template](macros.md#t_link_template) does a lot of work for us if we -provide the metadata it requires. - -```t_link_transactions.sql``` -```sql hl_lines="12 13 14 15 16 17" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='t_link') -}} - -{%- set source = [ref('stg_transactions_hashed')] -%} - -{%- set src_pk = 'TRANSACTION_PK' -%} -{%- set src_fk = ['CUSTOMER_FK', 'ORDER_FK'] -%} -{%- set src_payload = ['TRANSACTION_NUMBER', 'TRANSACTION_DATE', 'TYPE', 'AMOUNT'] -%} -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = source -%} -{%- set tgt_payload = source -%} -{%- set tgt_eff = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} -``` - -With these 6 additional lines, we have now informed the macro that we do not want to modify -our source data, we are simply using the ```source``` reference as shorthand for keeping the columns the same as -the source. In other tables in this walkthrough, notably [satellites](satellites.md#target-columns), we carried out -some manual mapping, but this isn't always necessary if we have all the columns we need in the staging layers. - ### Invoking the template -Now we bring it all together and call the [t_link_template](macros.md#t_link_template) macro: +Now we bring it all together and call the [t_link](macros.md#t_link) macro: ```t_link_transactions.sql``` -```sql hl_lines="19 20 21" -{{- config(materialized='incremental', schema='MYSCHEMA', enabled=true, tags='t_link') -}} - -{%- set source = [ref('stg_transactions_hashed')] -%} - -{%- set src_pk = 'TRANSACTION_PK' -%} -{%- set src_fk = ['CUSTOMER_FK', 'ORDER_FK'] -%} -{%- set src_payload = ['TRANSACTION_NUMBER', 'TRANSACTION_DATE', 'TYPE', 'AMOUNT'] -%} -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = source -%} -{%- set tgt_payload = source -%} -{%- set tgt_eff = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, - source) }} +```sql hl_lines="3 4 5" +{{- config(materialized='incremental', schema='VLT', tags='t_link') -}} + +{{ dbtvault.t_link(var('src_pk'), var('src_fk'), var('src_payload'), + var('src_eff'), var('src_ldts'), var('src_source'), + var('source')) }} ``` ### Running dbt diff --git a/docs/walkthrough.md b/docs/walkthrough.md index 67ba332dd..0bce8665d 100644 --- a/docs/walkthrough.md +++ b/docs/walkthrough.md @@ -14,17 +14,17 @@ We will: - process a raw staging layer. - create a Data Vault with hubs, links and satellites using dbtvault. -## Prerequisites +## Pre-requisites 1. Some prior knowledge of Data Vault 2.0 architecture. Have a look at [How can I get up to speed on Data Vault 2.0?](index.md#how-can-i-get-up-to-speed-on-data-vault-20) 2. A Snowflake account, trial or otherwise. [Sign up for a free 30-day trial here](https://trial.snowflake.com/ab/) -3. You must have downloaded and installed dbt 0.15, +3. You must have downloaded and installed dbt {{ config.extra.req_dbt_version }}, and [set up a project](https://docs.getdbt.com/v0.15.0/docs/dbt-projects). -4. Sources should be set up in dbt [(see below)](#setting-up-sources). +4. Sources should be set up in dbt [(see below)](walkthrough.md#setting-up-sources). 5. We assume you already have a raw staging layer. @@ -41,8 +41,8 @@ data much easier, cleaner and more modular. We have provided an example below which shows a configuration similar to that used for the examples in our documentation, however this feature is documented extensively in [the documentation for dbt](https://docs.getdbt.com/v0.15.0/docs/using-sources). -After reading the above documentation, we recommend that you place the ```schema.yml``` file you create for your sources, -in the root of your ```models``` folder, however you can place it where needed for your specific project and models. +We recommend that you place the ```schema.yml``` file you create for your sources, +in the root of your ```models``` folder, however you can place it wherever needed for your specific project and models. ```schema.yml``` @@ -67,7 +67,9 @@ Add the following to your ```packages.yml```: packages: - git: "https://github.com/Datavault-UK/dbtvault" + revision: v0.5 ``` + And run ```dbt deps``` diff --git a/macros/internal/check_relation.sql b/macros/internal/check_relation.sql index 439e8d936..88c398e93 100644 --- a/macros/internal/check_relation.sql +++ b/macros/internal/check_relation.sql @@ -1,4 +1,4 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault +{#- Copyright 2020 Business Thinking LTD. trading as Datavault Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/macros/internal/get_src_col_list.sql b/macros/internal/get_src_col_list.sql new file mode 100644 index 000000000..1abda8ac7 --- /dev/null +++ b/macros/internal/get_src_col_list.sql @@ -0,0 +1,43 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro get_src_col_list(tgt_cols) -%} + + +{%- set col_list = [] -%} + +{%- if tgt_cols is iterable -%} + + {%- for columns in tgt_cols -%} + + {%- if columns is string -%} + + {%- set _ = col_list.append(columns) -%} + + {#- If list of lists -#} + {%- elif columns is iterable and columns is not string -%} + + {%- for cols in columns -%} + + {%- set _ = col_list.append(cols) -%} + + {%- endfor -%} + {%- endif -%} + + {%- endfor -%} +{%- endif -%} + +{{ return(col_list) }} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/is_multi_source.sql b/macros/internal/is_multi_source.sql new file mode 100644 index 000000000..c31fb870e --- /dev/null +++ b/macros/internal/is_multi_source.sql @@ -0,0 +1,43 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} + +{%- macro is_multi_source(source, src_pk, src_fk, src_ldts, src_source) -%} + +{%- if source is iterable and source is not string -%} + {%- set multi_source = [] -%} + {%- for element in source -%} + + {%- set _ = multi_source.append(ref(element)) -%} + + {%- endfor -%} + + {%- set is_union = dbtvault.is_union(multi_source) -%} + {%- set source_col = dbtvault.source_columns(src_pk, src_fk, src_ldts, src_source, + multi_source, is_union) -%} + + {{- return([source_col, is_union]) -}} + +{%- else -%} + + {%- set source = [ref(var('source'))] -%} + {%- set is_union = dbtvault.is_union(source) -%} + {%- set source_col = dbtvault.source_columns(src_pk, src_fk, src_ldts, src_source, + source, is_union) -%} + + {{- return([source_col, is_union]) -}} + +{%- endif -%} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/is_union.sql b/macros/internal/is_union.sql index 088c13cd7..b87045145 100644 --- a/macros/internal/is_union.sql +++ b/macros/internal/is_union.sql @@ -1,4 +1,4 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault +{#- Copyright 2020 Business Thinking LTD. trading as Datavault Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/macros/internal/new_union.sql b/macros/internal/new_union.sql new file mode 100644 index 000000000..cf8c53a74 --- /dev/null +++ b/macros/internal/new_union.sql @@ -0,0 +1,37 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro new_union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -%} + + SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'src')}}, + LAG({{ src_source }}, 1) + OVER(PARTITION by {{ tgt_pk }} + ORDER BY {{ tgt_pk }}) AS FIRST_SOURCE + FROM ( + + {%- set letters='abcdefghijklmnopqrstuvwxyz' -%} + + {%- set iterations = source|length -%} + + {%- for src in range(iterations) -%} + {%- set letter = letters[loop.index0] %} + {{ dbtvault.single(src_pk, src_nk, src_ldts, src_source, + source[loop.index0], letter) -}} + + {% if not loop.last %} + UNION + {%- endif -%} + {%- endfor %} + ) AS src +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/retrieve_tgt_cols.sql b/macros/internal/retrieve_tgt_cols.sql new file mode 100644 index 000000000..5614cb556 --- /dev/null +++ b/macros/internal/retrieve_tgt_cols.sql @@ -0,0 +1,102 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro retrieve_tgt_cols() -%} + +{%- set tgt_pk = [ ref(kwargs['tgt_pk']|default(None, true)) ] -%} +{%- set tgt_nk = [ ref(kwargs['tgt_nk']|default(None, true))] -%} +{%- set tgt_fk = kwargs['tgt_fk']|default(None, true) -%} +{%- set tgt_payload = kwargs['tgt_payload']|default(None, true) -%} +{%- set tgt_hashdiff = kwargs['tgt_hashdiff']|default(None, true) -%} +{%- set tgt_eff = kwargs['tgt_eff']|default(None, true) -%} +{%- set tgt_ldts = [ ref(kwargs['tgt_ldts']|default(None, true)) ] -%} +{%- set tgt_source = [ ref(kwargs['tgt_source']|default(None, true)) ] -%} + +{%- set src_pk = kwargs['src_pk']|default(None, true) -%} +{%- set src_nk = kwargs['src_nk']|default(None, true) -%} +{%- set src_fk = kwargs['src_fk']|default(None, true) -%} +{%- set src_payload = kwargs['src_payload']|default(None, true) -%} +{%- set src_hashdiff = kwargs['src_hashdiff']|default(None, true) -%} +{%- set src_eff = kwargs['src_eff']|default(None, true) -%} +{%- set src_ldts = kwargs['src_ldts']|default(None, true) -%} +{%- set src_source = kwargs['src_source']|default(None, true) -%} + +{%- set source = kwargs['source']|default(None, true) -%} + +{%- set tgt_cols_dict = {'tgt_pk': (src_pk, tgt_pk, dbtvault.check_relation(tgt_pk[0])), + 'tgt_nk': (src_nk, tgt_nk, dbtvault.check_relation(tgt_nk[0])), + 'tgt_fk': (src_fk, tgt_fk, dbtvault.check_relation(tgt_fk[0])), + 'tgt_payload': (src_payload, tgt_payload, dbtvault.check_relation(tgt_payload[0])), + 'tgt_hashdiff': (src_hashdiff, tgt_hashdiff, dbtvault.check_relation(tgt_hashdiff[0])), + 'tgt_eff': (src_eff, tgt_eff, dbtvault.check_relation(tgt_eff[0])), + 'tgt_ldts': (src_ldts, tgt_ldts, dbtvault.check_relation(tgt_ldts[0])), + 'tgt_source': (src_source, tgt_source, dbtvault.check_relation(tgt_source[0]))} -%} + +{%- set tgt_cols_output = {'tgt_pk': '', + 'tgt_nk': '', + 'tgt_fk': '', + 'tgt_payload': '', + 'tgt_hashdiff': '', + 'tgt_eff': '', + 'tgt_ldts': '', + 'tgt_source': ''} -%} + +{%- set src_cols_list = dbtvault.get_col_list([src_pk, src_nk, src_fk, + src_payload, src_hashdiff, src_eff, + src_ldts, src_source] | reject("none") | list) -%} + +{%- set columns = adapter.get_columns_in_relation(source[0]) -%} +{%- set column_names = columns | map(attribute='name') | list -%} + +{{ dbtvault.validate_columns(src_cols_list, column_names, source[0]) }} + +{%- for col in tgt_cols_dict -%} + + {%- set src_cols = tgt_cols_dict[col][0] -%} + {%- set tgt_col = tgt_cols_dict[col][1] -%} + {%- set is_relation = tgt_cols_dict[col][2] -%} + {%- set tgt_col_list = [] -%} + + {%- if is_relation -%} + + {#- Add column triples to list -#} + {%- if src_cols is iterable and src_cols is not string -%} + {%- for src_col in src_cols -%} + {%- if src_col in column_names -%} + {%- set col_type = columns | selectattr('name', "equalto", src_col) | map(attribute='data_type') | list | default(" ", true) -%} + + {%- set _ = tgt_col_list.append([src_col, col_type[0], src_col]) -%} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {%- set col_type = columns | selectattr('name', "equalto", src_cols) | map(attribute='data_type' ) | list | default(" ", true) -%} + + {%- set _ = tgt_col_list.append([src_cols, col_type[0], src_cols]) -%} + {%- endif -%} + + {%- if tgt_col_list | length > 1 -%} + {%- set _ = tgt_cols_output.update({col: tgt_col_list}) -%} + {%- else -%} + {%- set _ = tgt_cols_output.update({col: tgt_col_list[0]}) -%} + {%- endif -%} + + {%- else -%} + {%- set _ = tgt_cols_output.update({col: tgt_col}) -%} + {%- endif -%} + +{% endfor %} + +{{ return(tgt_cols_output) }} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/single.sql b/macros/internal/single.sql index 373ab7fd3..00b106a64 100644 --- a/macros/internal/single.sql +++ b/macros/internal/single.sql @@ -1,4 +1,4 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault +{#- Copyright 2020 Business Thinking LTD. trading as Datavault Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,4 +17,5 @@ SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], letter) }} FROM {{ source }} AS {{ letter }} + {%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/source_columns.sql b/macros/internal/source_columns.sql new file mode 100644 index 000000000..bf64892cf --- /dev/null +++ b/macros/internal/source_columns.sql @@ -0,0 +1,28 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro source_columns(src_pk, src_nk, src_ldts, src_source, + source, is_union) -%} + + {%- if not is_union -%} + + {{- dbtvault.single(src_pk, src_nk, src_ldts, src_source, source[0], 'a') -}} + + {%- else -%} + + {{- dbtvault.new_union(src_pk, src_nk, src_ldts, src_source, src_pk, source) -}} + + {%- endif -%} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/validate_columns.sql b/macros/internal/validate_columns.sql index b01282539..4dea7c1aa 100644 --- a/macros/internal/validate_columns.sql +++ b/macros/internal/validate_columns.sql @@ -1,4 +1,4 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault +{#- Copyright 2020 Business Thinking LTD. trading as Datavault Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/macros/internal_deprecated/create_source.sql b/macros/internal_deprecated/create_source.sql new file mode 100644 index 000000000..29dc10e31 --- /dev/null +++ b/macros/internal_deprecated/create_source.sql @@ -0,0 +1,29 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro create_source(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source, is_union) -%} + + {%- if not is_union -%} + + {{- dbtvault.single(src_pk, src_nk, src_ldts, src_source, source[0], 'a') -}} + + {%- else -%} + + {{- dbtvault.union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -}} + + {%- endif -%} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal_deprecated/create_tgt_cols.sql b/macros/internal_deprecated/create_tgt_cols.sql new file mode 100644 index 000000000..658aeedd0 --- /dev/null +++ b/macros/internal_deprecated/create_tgt_cols.sql @@ -0,0 +1,102 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro create_tgt_cols() -%} + +{%- set tgt_pk = kwargs['tgt_pk']|default(None, true) -%} +{%- set tgt_nk = kwargs['tgt_nk']|default(None, true) -%} +{%- set tgt_fk = kwargs['tgt_fk']|default(None, true) -%} +{%- set tgt_payload = kwargs['tgt_payload']|default(None, true) -%} +{%- set tgt_hashdiff = kwargs['tgt_hashdiff']|default(None, true) -%} +{%- set tgt_eff = kwargs['tgt_eff']|default(None, true) -%} +{%- set tgt_ldts = kwargs['tgt_ldts']|default(None, true) -%} +{%- set tgt_source = kwargs['tgt_source']|default(None, true) -%} + +{%- set src_pk = kwargs['src_pk']|default(None, true) -%} +{%- set src_nk = kwargs['src_nk']|default(None, true) -%} +{%- set src_fk = kwargs['src_fk']|default(None, true) -%} +{%- set src_payload = kwargs['src_payload']|default(None, true) -%} +{%- set src_hashdiff = kwargs['src_hashdiff']|default(None, true) -%} +{%- set src_eff = kwargs['src_eff']|default(None, true) -%} +{%- set src_ldts = kwargs['src_ldts']|default(None, true) -%} +{%- set src_source = kwargs['src_source']|default(None, true) -%} + +{%- set source = kwargs['source']|default(None, true) -%} + +{%- set tgt_cols_dict = {'tgt_pk': (src_pk, tgt_pk, dbtvault.check_relation(tgt_pk[0])), + 'tgt_nk': (src_nk, tgt_nk, dbtvault.check_relation(tgt_nk[0])), + 'tgt_fk': (src_fk, tgt_fk, dbtvault.check_relation(tgt_fk[0])), + 'tgt_payload': (src_payload, tgt_payload, dbtvault.check_relation(tgt_payload[0])), + 'tgt_hashdiff': (src_hashdiff, tgt_hashdiff, dbtvault.check_relation(tgt_hashdiff[0])), + 'tgt_eff': (src_eff, tgt_eff, dbtvault.check_relation(tgt_eff[0])), + 'tgt_ldts': (src_ldts, tgt_ldts, dbtvault.check_relation(tgt_ldts[0])), + 'tgt_source': (src_source, tgt_source, dbtvault.check_relation(tgt_source[0]))} -%} + +{%- set tgt_cols_output = {'tgt_pk': '', + 'tgt_nk': '', + 'tgt_fk': '', + 'tgt_payload': '', + 'tgt_hashdiff': '', + 'tgt_eff': '', + 'tgt_ldts': '', + 'tgt_source': ''} -%} + +{%- set src_cols_list = dbtvault.get_col_list([src_pk, src_nk, src_fk, + src_payload, src_hashdiff, src_eff, + src_ldts, src_source] | reject("none") | list) -%} + +{%- set columns = adapter.get_columns_in_relation(source[0]) -%} +{%- set column_names = columns | map(attribute='name') | list -%} + +{{ dbtvault.validate_columns(src_cols_list, column_names, source[0]) }} + +{%- for col in tgt_cols_dict -%} + + {%- set src_cols = tgt_cols_dict[col][0] -%} + {%- set tgt_col = tgt_cols_dict[col][1] -%} + {%- set is_relation = tgt_cols_dict[col][2] -%} + {%- set tgt_col_list = [] -%} + + {%- if is_relation -%} + + {#- Add column triples to list -#} + {%- if src_cols is iterable and src_cols is not string -%} + {%- for src_col in src_cols -%} + {%- if src_col in column_names -%} + {%- set col_type = columns | selectattr('name', "equalto", src_col) | map(attribute='data_type') | list | default(" ", true) -%} + + {%- set _ = tgt_col_list.append([src_col, col_type[0], src_col]) -%} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {%- set col_type = columns | selectattr('name', "equalto", src_cols) | map(attribute='data_type' ) | list | default(" ", true) -%} + + {%- set _ = tgt_col_list.append([src_cols, col_type[0], src_cols]) -%} + {%- endif -%} + + {%- if tgt_col_list | length > 1 -%} + {%- set _ = tgt_cols_output.update({col: tgt_col_list}) -%} + {%- else -%} + {%- set _ = tgt_cols_output.update({col: tgt_col_list[0]}) -%} + {%- endif -%} + + {%- else -%} + {%- set _ = tgt_cols_output.update({col: tgt_col}) -%} + {%- endif -%} + +{% endfor %} + +{{ return(tgt_cols_output) }} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal_deprecated/get_col_list.sql b/macros/internal_deprecated/get_col_list.sql new file mode 100644 index 000000000..320eaa0b4 --- /dev/null +++ b/macros/internal_deprecated/get_col_list.sql @@ -0,0 +1,48 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro get_col_list(tgt_cols) -%} + + +{%- set col_list = [] -%} + +{%- if tgt_cols is iterable -%} + + {%- for columns in tgt_cols -%} + + {%- if columns is string -%} + + {%- set _ = col_list.append(columns) -%} + + {#- If a triple -#} + {%- elif columns | first is string -%} + + {%- set _ = col_list.append(columns|last) -%} + + {#- If list of lists -#} + {%- elif columns is iterable and columns is not string -%} + + {%- for cols in columns -%} + + {%- set _ = col_list.append(cols|last) -%} + + {%- endfor -%} + {%- endif -%} + + {%- endfor -%} +{%- endif -%} + +{{ return(col_list) }} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal_deprecated/union.sql b/macros/internal_deprecated/union.sql new file mode 100644 index 000000000..78ab0cb5f --- /dev/null +++ b/macros/internal_deprecated/union.sql @@ -0,0 +1,36 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -%} + + SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'src')}}, + LAG({{ src_source }}, 1) + OVER(PARTITION by {{ tgt_pk | last }} + ORDER BY {{ tgt_pk | last }}) AS FIRST_SOURCE + FROM ( + + {%- set letters='abcdefghijklmnopqrstuvwxyz' -%} + + {%- set iterations = source|length -%} + + {%- for src in range(iterations) -%} + {%- set letter = letters[loop.index0] %} + {{ dbtvault.single(src_pk, src_nk, src_ldts, src_source, + source[loop.index0], letter) -}} + {% if not loop.last %} + UNION + {%- endif -%} + {%- endfor %} + ) AS src +{%- endmacro -%} \ No newline at end of file diff --git a/macros/staging/add_columns.sql b/macros/staging/add_columns.sql index 408663034..97317ef67 100644 --- a/macros/staging/add_columns.sql +++ b/macros/staging/add_columns.sql @@ -1,4 +1,4 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault +{#- Copyright 2020 Business Thinking LTD. trading as Datavault Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/macros/staging/from.sql b/macros/staging/from.sql index cb287b710..fac6a9be9 100644 --- a/macros/staging/from.sql +++ b/macros/staging/from.sql @@ -1,4 +1,4 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault +{#- Copyright 2020 Business Thinking LTD. trading as Datavault Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/macros/staging/multi_hash.sql b/macros/staging/multi_hash.sql index 81fd8a5a1..3100e8112 100644 --- a/macros/staging/multi_hash.sql +++ b/macros/staging/multi_hash.sql @@ -1,4 +1,4 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault +{#- Copyright 2020 Business Thinking LTD. trading as Datavault Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ limitations under the License. -#} {%- macro multi_hash(triples) -%} --- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault +-- Generated by dbtvault. Copyright 2020 Business Thinking LTD. trading as Datavault SELECT {% for triple in triples -%} {%- if triple | length == 2 -%} diff --git a/macros/supporting/cast.sql b/macros/supporting/cast.sql index f9e014af1..fad2b3cdf 100644 --- a/macros/supporting/cast.sql +++ b/macros/supporting/cast.sql @@ -1,4 +1,4 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault +{#- Copyright 2020 Business Thinking LTD. trading as Datavault Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/macros/supporting/hash.sql b/macros/supporting/hash.sql index a9130334d..2d069b72f 100644 --- a/macros/supporting/hash.sql +++ b/macros/supporting/hash.sql @@ -1,4 +1,4 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault +{#- Copyright 2020 Business Thinking LTD. trading as Datavault Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ {%- set hash_size = 32 -%} {%- else -%} {%- set hash_alg = 'MD5_BINARY' -%} - {%- set hash_size = 32 -%} + {%- set hash_size = 16 -%} {%- endif -%} {#- Alpha sort columns before hashing -#} @@ -34,7 +34,7 @@ {%- endif -%} {%- if columns is string %} - CAST({{- hash_alg -}}(UPPER(TRIM(CAST({{columns}} AS VARCHAR)))) AS BINARY({{- hash_size -}})) AS {{alias}} + CAST({{- hash_alg -}}(IFNULL((UPPER(TRIM(CAST({{columns}} AS VARCHAR)))), '^^')) AS BINARY({{- hash_size -}})) AS {{alias}} {%- else %} diff --git a/macros/supporting/prefix.sql b/macros/supporting/prefix.sql index feb7f61a7..4d8c37d40 100644 --- a/macros/supporting/prefix.sql +++ b/macros/supporting/prefix.sql @@ -1,4 +1,4 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault +{#- Copyright 2020 Business Thinking LTD. trading as Datavault Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/macros/tables/hub.sql b/macros/tables/hub.sql new file mode 100644 index 000000000..cf3e8017b --- /dev/null +++ b/macros/tables/hub.sql @@ -0,0 +1,39 @@ +{#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro hub(src_pk, src_nk, src_ldts, src_source, + source) -%} + +{%- set source_data = dbtvault.is_multi_source(source, src_pk, src_nk, src_ldts, src_source) -%} +{%- set source_col = source_data[0] -%} +{%- set is_union = source_data[1] -%} + +-- Generated by dbtvault. +SELECT DISTINCT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'stg') }} +FROM ( + {{ source_col }} +) AS stg +{# If incremental union or single #} +{%- if is_incremental() -%} +LEFT JOIN {{ this }} AS tgt +ON {{ dbtvault.prefix([src_pk], 'stg') }} = {{ dbtvault.prefix([src_pk], 'tgt') }} +WHERE {{ dbtvault.prefix([src_pk], 'tgt') }} IS NULL +{# If an incremental and union load -#} +{% if is_union -%} +AND stg.FIRST_SOURCE IS NULL +{%- endif -%} +{%- endif -%} +{# If a union base-load #} +{%- if is_union and not is_incremental() -%} +WHERE stg.FIRST_SOURCE IS NULL +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/link.sql b/macros/tables/link.sql new file mode 100644 index 000000000..3014f44e0 --- /dev/null +++ b/macros/tables/link.sql @@ -0,0 +1,39 @@ +{#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro link(src_pk, src_fk, src_ldts, src_source, + source) -%} + +{%- set source_data = dbtvault.is_multi_source(source, src_pk, src_fk, src_ldts, src_source) -%} +{%- set source_col = source_data[0] -%} +{%- set is_union = source_data[1] -%} + +-- Generated by dbtvault. +SELECT DISTINCT {{ dbtvault.prefix([src_pk, src_fk, src_ldts, src_source], 'stg') }} +FROM ( + {{ source_col }} +) AS stg +{# If incremental union or single #} +{%- if is_incremental() -%} +LEFT JOIN {{ this }} AS tgt +ON {{ dbtvault.prefix([src_pk], 'stg') }} = {{ dbtvault.prefix([src_pk], 'tgt') }} +WHERE {{ dbtvault.prefix([src_pk], 'tgt') }} IS NULL +{# If an incremental and union load -#} +{% if is_union -%} +AND stg.FIRST_SOURCE IS NULL +{%- endif -%} +{%- endif -%} +{# If a union base-load #} +{%- if is_union and not is_incremental() -%} +WHERE stg.FIRST_SOURCE IS NULL +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/sat.sql b/macros/tables/sat.sql new file mode 100644 index 000000000..6310d3484 --- /dev/null +++ b/macros/tables/sat.sql @@ -0,0 +1,43 @@ +{#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro sat(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + source) -%} + +{%- set source_cols = dbtvault.get_src_col_list([src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source]) -%} + +-- Generated by dbtvault. +SELECT DISTINCT {{ dbtvault.prefix(source_cols, 'e') }} +FROM {{ ref(source) }} AS e +{% if is_incremental() -%} +LEFT JOIN ( + SELECT {{ dbtvault.prefix(source_cols, 'd') }} + FROM ( + SELECT {{ dbtvault.prefix(source_cols, 'c') }}, + CASE WHEN RANK() + OVER (PARTITION BY {{ dbtvault.prefix([src_pk], 'c') }} + ORDER BY {{ dbtvault.prefix([src_ldts], 'c') }} DESC) = 1 + THEN 'Y' ELSE 'N' END CURR_FLG + FROM ( + SELECT {{ dbtvault.prefix(source_cols, 'a') }} + FROM {{ this }} as a + JOIN {{ ref(source) }} as b + ON {{ dbtvault.prefix([src_pk], 'a') }} = {{ dbtvault.prefix([src_pk], 'b') }} + ) as c + ) AS d +WHERE d.CURR_FLG = 'Y') AS src +ON {{ dbtvault.prefix([src_hashdiff], 'src') }} = {{ dbtvault.prefix([src_hashdiff], 'e') }} +WHERE {{ dbtvault.prefix([src_hashdiff], 'src') }} IS NULL +{%- endif -%} + +{% endmacro %} diff --git a/macros/tables/t_link.sql b/macros/tables/t_link.sql new file mode 100644 index 000000000..5600b4990 --- /dev/null +++ b/macros/tables/t_link.sql @@ -0,0 +1,28 @@ +{#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro t_link(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, source) -%} + +{%- set source_cols = dbtvault.get_src_col_list([src_pk, src_fk, src_payload, src_eff, src_ldts, src_source])-%} + +-- Generated by dbtvault. +SELECT DISTINCT {{ dbtvault.prefix(source_cols, 'stg') }} +FROM ( + SELECT {{ dbtvault.prefix(source_cols, 'stg') }} + FROM {{ ref(source) }} AS stg +) AS stg +{% if is_incremental() -%} +LEFT JOIN {{ this }} AS tgt +ON {{ dbtvault.prefix([src_pk], 'stg') }} = {{ dbtvault.prefix([src_pk], 'tgt') }} +WHERE {{ dbtvault.prefix([src_pk], 'tgt') }} IS NULL +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/tables_deprecated/hub_template.sql b/macros/tables_deprecated/hub_template.sql new file mode 100644 index 000000000..37bba42eb --- /dev/null +++ b/macros/tables_deprecated/hub_template.sql @@ -0,0 +1,50 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro hub_template(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source) -%} + +{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_nk=src_nk, src_ldts=src_ldts, src_source=src_source, + tgt_pk=tgt_pk, tgt_nk=tgt_nk, tgt_ldts=tgt_ldts, tgt_source=tgt_source, + source=source) -%} + +{%- set tgt_pk = tgt_cols['tgt_pk'] -%} +{%- set tgt_nk = tgt_cols['tgt_nk'] -%} +{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} +{%- set tgt_source = tgt_cols['tgt_source'] -%} + +{%- set is_union = dbtvault.is_union(source) -%} +-- Generated by dbtvault. Copyright 2020 Business Thinking LTD. trading as Datavault +SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_nk, tgt_ldts, tgt_source], 'stg') }} +FROM ( + {{ dbtvault.create_source(src_pk, src_nk, src_ldts, src_source, + tgt_pk, tgt_nk, tgt_ldts, tgt_source, + source, is_union) }} +) AS stg +{# If incremental union or single #} +{%- if is_incremental() -%} +LEFT JOIN {{ this }} AS tgt +ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} +WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL +{# If an incremental and union load -#} +{% if is_union -%} +AND stg.FIRST_SOURCE IS NULL +{%- endif -%} +{%- endif -%} +{# If a union base-load #} +{%- if is_union and not is_incremental() -%} +WHERE stg.FIRST_SOURCE IS NULL +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/tables_deprecated/link_template.sql b/macros/tables_deprecated/link_template.sql new file mode 100644 index 000000000..bcc6b431a --- /dev/null +++ b/macros/tables_deprecated/link_template.sql @@ -0,0 +1,50 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro link_template(src_pk, src_fk, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source) -%} + +{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_fk=src_fk, src_ldts=src_ldts, src_source=src_source, + tgt_pk=tgt_pk, tgt_fk=tgt_fk, tgt_ldts=tgt_ldts, tgt_source=tgt_source, + source=source) -%} + +{%- set tgt_pk = tgt_cols['tgt_pk'] -%} +{%- set tgt_fk = tgt_cols['tgt_fk'] -%} +{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} +{%- set tgt_source = tgt_cols['tgt_source'] -%} + +{%- set is_union = dbtvault.is_union(source) -%} +-- Generated by dbtvault. Copyright 2020 Business Thinking LTD. trading as Datavault +SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_ldts, tgt_source], 'stg') }} +FROM ( + {{ dbtvault.create_source(src_pk, src_fk, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_ldts, tgt_source, + source, is_union) }} +) AS stg +{# If incremental union or single #} +{%- if is_incremental() -%} +LEFT JOIN {{ this }} AS tgt +ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} +WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL +{# If an incremental and union load -#} +{% if is_union -%} +AND stg.FIRST_SOURCE IS NULL +{%- endif -%} +{%- endif -%} +{# If a union base-load #} +{%- if is_union and not is_incremental() -%} +WHERE stg.FIRST_SOURCE IS NULL +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/tables_deprecated/sat_template.sql b/macros/tables_deprecated/sat_template.sql new file mode 100644 index 000000000..9edb78c2b --- /dev/null +++ b/macros/tables_deprecated/sat_template.sql @@ -0,0 +1,62 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro sat_template(src_pk, src_hashdiff, src_payload, + src_eff, src_ldts, src_source, + tgt_pk, tgt_hashdiff, tgt_payload, + tgt_eff, tgt_ldts, tgt_source, + source) -%} + +{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, + src_hashdiff=src_hashdiff, src_payload=src_payload, src_eff=src_eff, + src_ldts=src_ldts, src_source=src_source, + tgt_pk=tgt_pk, + tgt_hashdiff=tgt_hashdiff, tgt_payload=tgt_payload, tgt_eff=tgt_eff, + tgt_ldts=tgt_ldts, tgt_source=tgt_source, + source=source) -%} + +{%- set tgt_pk = tgt_cols['tgt_pk'] -%} +{%- set tgt_hashdiff = tgt_cols['tgt_hashdiff'] -%} +{%- set tgt_payload = tgt_cols['tgt_payload'] -%} +{%- set tgt_eff = tgt_cols['tgt_eff'] -%} +{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} +{%- set tgt_source = tgt_cols['tgt_source'] -%} + +{%- set tgt_cols_list = dbtvault.get_col_list([tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source]) -%} + +-- Generated by dbtvault. Copyright 2020 Business Thinking LTD. trading as Datavault +SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_hashdiff, tgt_payload, tgt_ldts, tgt_eff, tgt_source], 'e') }} +FROM {{ source[0] }} AS e +{% if is_incremental() -%} +LEFT JOIN ( + SELECT {{ dbtvault.prefix(tgt_cols_list, 'd') }} + FROM ( + SELECT {{ dbtvault.prefix(tgt_cols_list, 'c') }}, + CASE WHEN RANK() + OVER (PARTITION BY {{ dbtvault.prefix([tgt_pk|last], 'c') }} + ORDER BY {{ dbtvault.prefix([tgt_ldts|last], 'c') }} DESC) = 1 + THEN 'Y' ELSE 'N' END CURR_FLG + FROM ( + SELECT {{ dbtvault.prefix(tgt_cols_list, 'a') }} + FROM {{ this }} as a + JOIN {{ source[0] }} as b + ON {{ dbtvault.prefix([tgt_pk|last], 'a') }} = {{ dbtvault.prefix([src_pk], 'b') }} + ) as c + ) AS d +WHERE d.CURR_FLG = 'Y') AS src +ON {{ dbtvault.prefix([tgt_hashdiff|last], 'src') }} = {{ dbtvault.prefix([src_hashdiff], 'e') }} +WHERE {{ dbtvault.prefix([tgt_hashdiff|last], 'src') }} IS NULL +{%- endif -%} + +{% endmacro %} diff --git a/macros/tables_deprecated/t_link_template.sql b/macros/tables_deprecated/t_link_template.sql new file mode 100644 index 000000000..61e9f9ed4 --- /dev/null +++ b/macros/tables_deprecated/t_link_template.sql @@ -0,0 +1,45 @@ +{#- Copyright 2020 Business Thinking LTD. trading as Datavault + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, + tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, + source) -%} + +{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_fk=src_fk, src_payload=src_payload, + src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, + tgt_pk=tgt_pk, tgt_fk=tgt_fk, tgt_payload=tgt_payload, + tgt_eff=tgt_eff, tgt_ldts=tgt_ldts, tgt_source=tgt_source, + source=source) -%} + +{%- set tgt_pk = tgt_cols['tgt_pk'] -%} +{%- set tgt_fk = tgt_cols['tgt_fk'] -%} +{%- set tgt_payload = tgt_cols['tgt_payload'] -%} +{%- set tgt_eff = tgt_cols['tgt_eff'] -%} +{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} +{%- set tgt_source = tgt_cols['tgt_source'] -%} + +{%- set is_union = dbtvault.is_union(source) -%} +-- Generated by dbtvault. Copyright 2020 Business Thinking LTD. trading as Datavault +SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source], 'stg') }} +FROM ( + SELECT {{ dbtvault.prefix([src_pk, src_fk, src_payload, src_eff, + src_ldts, src_source], 'stg') }} + FROM {{ source[0] }} AS stg +) AS stg +{% if is_incremental() -%} +LEFT JOIN {{ this }} AS tgt +ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} +WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 41a0f97da..bdde1f389 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,6 +18,7 @@ repo_url: 'https://github.com/Datavault-UK/dbtvault' nav: - Home: 'index.md' + - Metadata: 'metadata.md' - Walk-through guide: - Getting Started: 'walkthrough.md' - Staging: 'staging.md' @@ -32,6 +33,7 @@ nav: - Creating the stage layers: 'stagingdemo.md' - Loading the vault: 'loading.md' - Macros: 'macros.md' + - Migration from v0.4 to v0.5: 'migrating.md' - Best Practices: 'bestpractices.md' - Roadmap: 'roadmap.md' - Changelog: 'changelog.md' @@ -50,6 +52,8 @@ extra: link: 'https://www.linkedin.com/company/business-thinking-limited' - type: 'facebook' link: 'https://www.facebook.com/DataVaultUK/' + version: 0.5 + req_dbt_version: 0.15.x markdown_extensions: - codehilite: @@ -61,11 +65,15 @@ markdown_extensions: permalink: true toc_depth: 1-3 -#plugins: -# - search -# - pdf-export +plugins: + - search +# - markdownextradata: {} +# - pdf-export: +# combined: true + + extra_css: - 'stylesheets/extra.css' -copyright: dbtvault and documentation © Business Thinking trading as Datavault 2019 - Data Vault 2.0 is a registered trademark of Dan Linstedt - dbt is a registered trademark of Fishtown Analytics +copyright: dbtvault and documentation © Business Thinking trading as Datavault 2020 - Data Vault 2.0 is a registered trademark of Dan Linstedt - dbt is a registered trademark of Fishtown Analytics From 014d49b3d173daf5a18b005fa9b03f5b1cfda08d Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 24 Feb 2020 15:28:07 +0000 Subject: [PATCH 107/157] Updated README.md Version 0.5 --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1ad3f71ac..d6c269704 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

-[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.4.1)](https://dbtvault.readthedocs.io/en/v0.4.1/?badge=v0.4.1)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.5)](https://dbtvault.readthedocs.io/en/v0.5/?badge=v0.5)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) @@ -49,13 +49,13 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.4.1 # Latest stable version + revision: v0.5 # Latest stable version ``` And run ```dbt deps``` -[Read more on package installation](https://docs.getdbt.com/v0.14.0/docs/package-management) +[Read more on package installation](https://docs.getdbt.com/v0.15.0/docs/package-management) ## Usage @@ -66,13 +66,8 @@ And run ```bash {{- config(...) -}} -{%- set metadata = ... -%} - -{%- set source = ... -%} - -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} +{{ dbtvault.hub(var('src_pk'), var('src_nk'), var('src_ldts'), + var('src_source'), var('source')) }} ``` ## Sign up for early-bird announcements From 5ce8d70a759e456a4c5e3a833b9ef1b52bf60dc8 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 12 Mar 2020 17:07:23 +0000 Subject: [PATCH 108/157] Minor docs/config fixes. --- dbt_project.yml | 4 ++-- docs/walkthrough.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dbt_project.yml b/dbt_project.yml index cb4660dfc..cedec4187 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,6 +1,6 @@ name: 'dbtvault' -version: '0.4.1' -require-dbt-version: [">=0.14.0", "<=0.15.0"] +version: '0.5' +require-dbt-version: [">=0.14.0", "<=0.15.2"] profile: 'dbtvault' diff --git a/docs/walkthrough.md b/docs/walkthrough.md index 0bce8665d..8a9b76f65 100644 --- a/docs/walkthrough.md +++ b/docs/walkthrough.md @@ -21,7 +21,7 @@ We will: 2. A Snowflake account, trial or otherwise. [Sign up for a free 30-day trial here](https://trial.snowflake.com/ab/) -3. You must have downloaded and installed dbt {{ config.extra.req_dbt_version }}, +3. You must have downloaded and installed dbt 0.15.x, and [set up a project](https://docs.getdbt.com/v0.15.0/docs/dbt-projects). 4. Sources should be set up in dbt [(see below)](walkthrough.md#setting-up-sources). From 68a8ec01d849bfe5be59dcc9b7203b69688d3243 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 24 Mar 2020 22:12:43 +0000 Subject: [PATCH 109/157] Version 0.6 beta 1 THIS IS A BETA RELEASE, USE WITH CARE - Added effectivity satellites - Added limited documentation for effectivity satellites - Updated required dbt version to 0.16.0 --- dbt_project.yml | 4 +- docs/macros.md | 42 ++++++++++++ docs/metadata.md | 10 ++- docs/walkthrough.md | 4 +- macros/internal/create_source.sql | 29 -------- macros/internal/create_tgt_cols.sql | 102 ---------------------------- macros/internal/get_col_list.sql | 48 ------------- macros/internal/union.sql | 36 ---------- macros/tables/eff_sat.sql | 82 ++++++++++++++++++++++ macros/tables/hub_template.sql | 50 -------------- macros/tables/link_template.sql | 50 -------------- macros/tables/sat_template.sql | 62 ----------------- macros/tables/t_link_template.sql | 45 ------------ 13 files changed, 136 insertions(+), 428 deletions(-) delete mode 100644 macros/internal/create_source.sql delete mode 100644 macros/internal/create_tgt_cols.sql delete mode 100644 macros/internal/get_col_list.sql delete mode 100644 macros/internal/union.sql create mode 100644 macros/tables/eff_sat.sql delete mode 100644 macros/tables/hub_template.sql delete mode 100644 macros/tables/link_template.sql delete mode 100644 macros/tables/sat_template.sql delete mode 100644 macros/tables/t_link_template.sql diff --git a/dbt_project.yml b/dbt_project.yml index cedec4187..03ad7784e 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,6 +1,6 @@ name: 'dbtvault' -version: '0.5' -require-dbt-version: [">=0.14.0", "<=0.15.2"] +version: '0.6' +require-dbt-version: [">=0.14.0", "<0.17.0"] profile: 'dbtvault' diff --git a/docs/macros.md b/docs/macros.md index 3e2befca4..9034356ba 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -282,6 +282,48 @@ WHERE tgt.TRANSACTION_PK IS NULL ``` ___ +### eff_sat + +!!! tip "Cutting edge release" + **This feature is currently unreleased. Whilst it has been fully tested, we recommend that you use it with care.** + + If you find any bugs or would like to recommend improvements or additions, please + [submit an issue](https://github.com/Datavault-UK/dbtvault/issues). + +Generates sql to build a effectivity satellite table using the provided metadata in the dbt_project.yml. + +```jinja2 +{{ dbtvault.eff_sat(var('src_pk'), var('src_dfk'), var('src_sfk'), var('src_ldts'), + var('src_eff_from'), var('src_start_date'), var('src_end_date'), + var('src_source'), var('link'), var('source')) }} +``` + +#### Parameters + +| Parameter | Description | Type | Required? | +| -------------- | -------------------------------------------------------- | -------------- | ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | check_circle | +| src_dfk | Coming soon. | String | check_circle | +| src_sfk | Coming soon. | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_eff_from | Source effective from column | String | check_circle | +| src_start_date | The date which a link record is open/closed from | String | check_circle | +| src_end_date | The date which a link record is open/closed to | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| link | The link which this effectivity satellite is attached to | String | check_circle | +| source | Staging model reference or table name | String | check_circle | | | check_circle | + + +#### Usage + +Coming soon. + +#### Example output + +Coming soon. + +___ + ## Staging Macros ######(macros/staging) diff --git a/docs/metadata.md b/docs/metadata.md index 3f004517d..3d1bca504 100644 --- a/docs/metadata.md +++ b/docs/metadata.md @@ -1,10 +1,12 @@ As of v0.5, metadata is provided to the models through the ```dbt_project.yml``` file instead of being specified in the models themselves. This keeps the metadata all in one place and simplifies the use of dbtvault. +For further detail of the below table templates, see: [table templates](macros.md#table-templates). + !!! note In v0.5, only source column metadata is necessary, we have removed target column metadata. -#### Declaring sources +#### Declaring sources (in the metadata) Since v0.5, there is no longer the need to state the source using the ```ref``` macro, the new [macros](macros.md) do this all for you. For single source models, just state the name of the source as a string. @@ -125,7 +127,7 @@ The [t_link](macros.md#t_link) macro accepts the following parameters: | src_payload | The columns that make up and payload of the transactional link. The columns must be entered as a list of strings. | | src_eff | The effective from date column. | | src_ldts | The loaddate timestamp column of the record. | -| src_source |The source column of the record. | +| src_source | The source column of the record. | ```dbt_project.yml``` ```yaml @@ -146,6 +148,10 @@ t_link_transactions: src_source: 'SOURCE' ``` +#### Effectivity satellites + +Documentation coming soon. Please refer to [eff_sat](macros.md#eff_sat) in the meantime. + #### The problem with metadata As metadata is stored in the ```dbt_project.yml```, you can probably foresee the file getting very large for bigger diff --git a/docs/walkthrough.md b/docs/walkthrough.md index 8a9b76f65..f5a34e1ed 100644 --- a/docs/walkthrough.md +++ b/docs/walkthrough.md @@ -21,7 +21,7 @@ We will: 2. A Snowflake account, trial or otherwise. [Sign up for a free 30-day trial here](https://trial.snowflake.com/ab/) -3. You must have downloaded and installed dbt 0.15.x, +3. You must have downloaded and installed dbt 0.15.2, and [set up a project](https://docs.getdbt.com/v0.15.0/docs/dbt-projects). 4. Sources should be set up in dbt [(see below)](walkthrough.md#setting-up-sources). @@ -33,7 +33,7 @@ contains data for one ```load_datetime``` value only). **We will be removing thi 7. You should read our [best practices](bestpractices.md) guidance. -## Setting up sources +## Setting up sources (in dbt) We will be using the ```source``` feature of dbt extensively throughout the documentation to make access to source data much easier, cleaner and more modular. diff --git a/macros/internal/create_source.sql b/macros/internal/create_source.sql deleted file mode 100644 index 5f258143e..000000000 --- a/macros/internal/create_source.sql +++ /dev/null @@ -1,29 +0,0 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} -{%- macro create_source(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source, is_union) -%} - - {%- if not is_union -%} - - {{- dbtvault.single(src_pk, src_nk, src_ldts, src_source, source[0], 'a') -}} - - {%- else -%} - - {{- dbtvault.union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -}} - - {%- endif -%} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/create_tgt_cols.sql b/macros/internal/create_tgt_cols.sql deleted file mode 100644 index 1136015b6..000000000 --- a/macros/internal/create_tgt_cols.sql +++ /dev/null @@ -1,102 +0,0 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} -{%- macro create_tgt_cols() -%} - -{%- set tgt_pk = kwargs['tgt_pk']|default(None, true) -%} -{%- set tgt_nk = kwargs['tgt_nk']|default(None, true) -%} -{%- set tgt_fk = kwargs['tgt_fk']|default(None, true) -%} -{%- set tgt_payload = kwargs['tgt_payload']|default(None, true) -%} -{%- set tgt_hashdiff = kwargs['tgt_hashdiff']|default(None, true) -%} -{%- set tgt_eff = kwargs['tgt_eff']|default(None, true) -%} -{%- set tgt_ldts = kwargs['tgt_ldts']|default(None, true) -%} -{%- set tgt_source = kwargs['tgt_source']|default(None, true) -%} - -{%- set src_pk = kwargs['src_pk']|default(None, true) -%} -{%- set src_nk = kwargs['src_nk']|default(None, true) -%} -{%- set src_fk = kwargs['src_fk']|default(None, true) -%} -{%- set src_payload = kwargs['src_payload']|default(None, true) -%} -{%- set src_hashdiff = kwargs['src_hashdiff']|default(None, true) -%} -{%- set src_eff = kwargs['src_eff']|default(None, true) -%} -{%- set src_ldts = kwargs['src_ldts']|default(None, true) -%} -{%- set src_source = kwargs['src_source']|default(None, true) -%} - -{%- set source = kwargs['source']|default(None, true) -%} - -{%- set tgt_cols_dict = {'tgt_pk': (src_pk, tgt_pk, dbtvault.check_relation(tgt_pk[0])), - 'tgt_nk': (src_nk, tgt_nk, dbtvault.check_relation(tgt_nk[0])), - 'tgt_fk': (src_fk, tgt_fk, dbtvault.check_relation(tgt_fk[0])), - 'tgt_payload': (src_payload, tgt_payload, dbtvault.check_relation(tgt_payload[0])), - 'tgt_hashdiff': (src_hashdiff, tgt_hashdiff, dbtvault.check_relation(tgt_hashdiff[0])), - 'tgt_eff': (src_eff, tgt_eff, dbtvault.check_relation(tgt_eff[0])), - 'tgt_ldts': (src_ldts, tgt_ldts, dbtvault.check_relation(tgt_ldts[0])), - 'tgt_source': (src_source, tgt_source, dbtvault.check_relation(tgt_source[0]))} -%} - -{%- set tgt_cols_output = {'tgt_pk': '', - 'tgt_nk': '', - 'tgt_fk': '', - 'tgt_payload': '', - 'tgt_hashdiff': '', - 'tgt_eff': '', - 'tgt_ldts': '', - 'tgt_source': ''} -%} - -{%- set src_cols_list = dbtvault.get_col_list([src_pk, src_nk, src_fk, - src_payload, src_hashdiff, src_eff, - src_ldts, src_source] | reject("none") | list) -%} - -{%- set columns = adapter.get_columns_in_relation(source[0]) -%} -{%- set column_names = columns | map(attribute='name') | list -%} - -{{ dbtvault.validate_columns(src_cols_list, column_names, source[0]) }} - -{%- for col in tgt_cols_dict -%} - - {%- set src_cols = tgt_cols_dict[col][0] -%} - {%- set tgt_col = tgt_cols_dict[col][1] -%} - {%- set is_relation = tgt_cols_dict[col][2] -%} - {%- set tgt_col_list = [] -%} - - {%- if is_relation -%} - - {#- Add column triples to list -#} - {%- if src_cols is iterable and src_cols is not string -%} - {%- for src_col in src_cols -%} - {%- if src_col in column_names -%} - {%- set col_type = columns | selectattr('name', "equalto", src_col) | map(attribute='data_type') | list | default(" ", true) -%} - - {%- set _ = tgt_col_list.append([src_col, col_type[0], src_col]) -%} - {%- endif -%} - {%- endfor -%} - {%- else -%} - {%- set col_type = columns | selectattr('name', "equalto", src_cols) | map(attribute='data_type' ) | list | default(" ", true) -%} - - {%- set _ = tgt_col_list.append([src_cols, col_type[0], src_cols]) -%} - {%- endif -%} - - {%- if tgt_col_list | length > 1 -%} - {%- set _ = tgt_cols_output.update({col: tgt_col_list}) -%} - {%- else -%} - {%- set _ = tgt_cols_output.update({col: tgt_col_list[0]}) -%} - {%- endif -%} - - {%- else -%} - {%- set _ = tgt_cols_output.update({col: tgt_col}) -%} - {%- endif -%} - -{% endfor %} - -{{ return(tgt_cols_output) }} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/get_col_list.sql b/macros/internal/get_col_list.sql deleted file mode 100644 index 06f744ef5..000000000 --- a/macros/internal/get_col_list.sql +++ /dev/null @@ -1,48 +0,0 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} -{%- macro get_col_list(tgt_cols) -%} - - -{%- set col_list = [] -%} - -{%- if tgt_cols is iterable -%} - - {%- for columns in tgt_cols -%} - - {%- if columns is string -%} - - {%- set _ = col_list.append(columns) -%} - - {#- If a triple -#} - {%- elif columns | first is string -%} - - {%- set _ = col_list.append(columns|last) -%} - - {#- If list of lists -#} - {%- elif columns is iterable and columns is not string -%} - - {%- for cols in columns -%} - - {%- set _ = col_list.append(cols|last) -%} - - {%- endfor -%} - {%- endif -%} - - {%- endfor -%} -{%- endif -%} - -{{ return(col_list) }} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/union.sql b/macros/internal/union.sql deleted file mode 100644 index a474c182b..000000000 --- a/macros/internal/union.sql +++ /dev/null @@ -1,36 +0,0 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} -{%- macro union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -%} - - SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'src')}}, - LAG({{ src_source }}, 1) - OVER(PARTITION by {{ tgt_pk | last }} - ORDER BY {{ tgt_pk | last }}) AS FIRST_SOURCE - FROM ( - - {%- set letters='abcdefghijklmnopqrstuvwxyz' -%} - - {%- set iterations = source|length -%} - - {%- for src in range(iterations) -%} - {%- set letter = letters[loop.index0] %} - {{ dbtvault.single(src_pk, src_nk, src_ldts, src_source, - source[loop.index0], letter) -}} - {% if not loop.last %} - UNION - {%- endif -%} - {%- endfor %} - ) AS src -{%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/eff_sat.sql b/macros/tables/eff_sat.sql new file mode 100644 index 000000000..e035c9ee5 --- /dev/null +++ b/macros/tables/eff_sat.sql @@ -0,0 +1,82 @@ +{#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro eff_sat(src_pk, src_dfk, src_sfk, src_ldts, src_eff_from, src_start_date, src_end_date, src_source, link, source)-%} + +{%- set source_cols = dbtvault.get_src_col_list([src_pk, src_ldts, src_eff_from, src_start_date, src_end_date, src_source])-%} +{%- set max_date = "'" ~ '9999-12-31' ~ "'" -%} + +WITH +{#- Reduce data set to size of stage table. #} +c AS (SELECT DISTINCT + {{ dbtvault.prefix(source_cols, 'a') }} + FROM {{ this }} AS a + INNER JOIN {{ ref(source) }} AS b ON {{ dbtvault.prefix([src_pk], 'a') }}={{ dbtvault.prefix([src_pk], 'b') }} + ) +{# Find latest satellite for each pk in set c. -#} +, d as (SELECT + {{ dbtvault.prefix(source_cols, 'c') }}, + CASE WHEN RANK() + OVER (PARTITION BY {{ dbtvault.prefix([src_pk], 'c') }} + ORDER BY {{ dbtvault.prefix([src_end_date], 'c') }} ASC) = 1 + THEN 'Y' ELSE 'N' END AS CURR_FLG + FROM c) +, p AS ( + SELECT q.* FROM {{ ref(link) }} AS q + INNER JOIN {{ ref(source) }} AS r ON {{ dbtvault.prefix([src_dfk], 'q') }}={{ dbtvault.prefix([src_dfk], 'r') }} +) +, x AS ( + SELECT p.*, {{ dbtvault.prefix([src_dfk], 's') }} AS STG_CUSTOMER_FK + FROM p + LEFT JOIN {{ ref(source) }} AS s ON {{ dbtvault.prefix([src_dfk], 'p') }}={{ dbtvault.prefix([src_dfk], 's') }} + AND {{ dbtvault.prefix([src_sfk], 'p') }}={{ dbtvault.prefix([src_sfk], 's') }} + WHERE ({{ dbtvault.prefix([src_dfk], 's') }} IS NULL AND {{ dbtvault.prefix([src_sfk], 's') }} IS NULL) +) +, y AS ( + SELECT + {{ dbtvault.prefix([src_pk, src_ldts, src_source, src_eff_from, src_start_date, src_end_date], 't') }}, + {{ dbtvault.prefix(['STG_CUSTOMER_FK'], 'x') }}, + {{ dbtvault.prefix([src_dfk], 'x')}}, + CASE WHEN RANK() + OVER (PARTITION BY {{ dbtvault.prefix([src_pk], 't') }} + ORDER BY {{ dbtvault.prefix([src_end_date], 't') }} ASC) = 1 + THEN 'Y' ELSE 'N' END AS CURR_FLG + FROM x + INNER JOIN {{ this }} AS t ON {{ dbtvault.prefix([src_pk], 'x') }}={{ dbtvault.prefix([src_pk], 't') }} + ) + +SELECT DISTINCT + {{ dbtvault.prefix([src_pk, src_ldts, src_source, src_eff_from], 'e') }}, + {{ dbtvault.prefix([src_eff_from], 'e') }} AS {{ src_start_date }}, + {{ dbtvault.prefix([src_end_date], 'e') }} +FROM {{ ref(source) }} AS e +{% if is_incremental() -%} +LEFT JOIN ( + SELECT {{ dbtvault.prefix(source_cols, 'd')}} + FROM d + WHERE d.CURR_FLG = 'Y' AND {{ dbtvault.prefix([src_end_date], 'd') }}=TO_DATE({{ max_date }}) + ) AS eff +ON {{ dbtvault.prefix([src_pk], 'eff') }}={{ dbtvault.prefix([src_pk], 'e') }} +WHERE {{ dbtvault.prefix([src_pk], 'eff') }} IS NULL +UNION +SELECT + {{ dbtvault.prefix([src_pk], 'y') }}, + {{ dbtvault.prefix([src_ldts], 'z') }}, + {{ dbtvault.prefix([src_source, src_eff_from, src_start_date], 'y') }}, + CASE WHEN y.STG_CUSTOMER_FK IS NULL + THEN {{ dbtvault.prefix([src_eff_from], 'z') }} ELSE {{ max_date }} END AS {{ src_end_date }} +FROM y +LEFT JOIN {{ ref(source) }} AS z ON {{ dbtvault.prefix([src_dfk], 'y') }}={{ dbtvault.prefix([src_dfk], 'z') }} +WHERE y.CURR_FLG='Y' AND {{ dbtvault.prefix([src_end_date], 'y') }}={{ max_date }} +{%- endif -%} + +{% endmacro %} \ No newline at end of file diff --git a/macros/tables/hub_template.sql b/macros/tables/hub_template.sql deleted file mode 100644 index 0a7cbe518..000000000 --- a/macros/tables/hub_template.sql +++ /dev/null @@ -1,50 +0,0 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} -{%- macro hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) -%} - -{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_nk=src_nk, src_ldts=src_ldts, src_source=src_source, - tgt_pk=tgt_pk, tgt_nk=tgt_nk, tgt_ldts=tgt_ldts, tgt_source=tgt_source, - source=source) -%} - -{%- set tgt_pk = tgt_cols['tgt_pk'] -%} -{%- set tgt_nk = tgt_cols['tgt_nk'] -%} -{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} -{%- set tgt_source = tgt_cols['tgt_source'] -%} - -{%- set is_union = dbtvault.is_union(source) -%} --- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault -SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_nk, tgt_ldts, tgt_source], 'stg') }} -FROM ( - {{ dbtvault.create_source(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source, is_union) }} -) AS stg -{# If incremental union or single #} -{%- if is_incremental() -%} -LEFT JOIN {{ this }} AS tgt -ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} -WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL -{# If an incremental and union load -#} -{% if is_union -%} -AND stg.FIRST_SOURCE IS NULL -{%- endif -%} -{%- endif -%} -{# If a union base-load #} -{%- if is_union and not is_incremental() -%} -WHERE stg.FIRST_SOURCE IS NULL -{%- endif -%} -{%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/link_template.sql b/macros/tables/link_template.sql deleted file mode 100644 index c02dc62e5..000000000 --- a/macros/tables/link_template.sql +++ /dev/null @@ -1,50 +0,0 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} -{%- macro link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) -%} - -{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_fk=src_fk, src_ldts=src_ldts, src_source=src_source, - tgt_pk=tgt_pk, tgt_fk=tgt_fk, tgt_ldts=tgt_ldts, tgt_source=tgt_source, - source=source) -%} - -{%- set tgt_pk = tgt_cols['tgt_pk'] -%} -{%- set tgt_fk = tgt_cols['tgt_fk'] -%} -{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} -{%- set tgt_source = tgt_cols['tgt_source'] -%} - -{%- set is_union = dbtvault.is_union(source) -%} --- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault -SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_ldts, tgt_source], 'stg') }} -FROM ( - {{ dbtvault.create_source(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source, is_union) }} -) AS stg -{# If incremental union or single #} -{%- if is_incremental() -%} -LEFT JOIN {{ this }} AS tgt -ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} -WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL -{# If an incremental and union load -#} -{% if is_union -%} -AND stg.FIRST_SOURCE IS NULL -{%- endif -%} -{%- endif -%} -{# If a union base-load #} -{%- if is_union and not is_incremental() -%} -WHERE stg.FIRST_SOURCE IS NULL -{%- endif -%} -{%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/sat_template.sql b/macros/tables/sat_template.sql deleted file mode 100644 index 746663d44..000000000 --- a/macros/tables/sat_template.sql +++ /dev/null @@ -1,62 +0,0 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} -{%- macro sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - source) -%} - -{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, - src_hashdiff=src_hashdiff, src_payload=src_payload, src_eff=src_eff, - src_ldts=src_ldts, src_source=src_source, - tgt_pk=tgt_pk, - tgt_hashdiff=tgt_hashdiff, tgt_payload=tgt_payload, tgt_eff=tgt_eff, - tgt_ldts=tgt_ldts, tgt_source=tgt_source, - source=source) -%} - -{%- set tgt_pk = tgt_cols['tgt_pk'] -%} -{%- set tgt_hashdiff = tgt_cols['tgt_hashdiff'] -%} -{%- set tgt_payload = tgt_cols['tgt_payload'] -%} -{%- set tgt_eff = tgt_cols['tgt_eff'] -%} -{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} -{%- set tgt_source = tgt_cols['tgt_source'] -%} - -{%- set tgt_cols_list = dbtvault.get_col_list([tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source]) -%} - --- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault -SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_hashdiff, tgt_payload, tgt_ldts, tgt_eff, tgt_source], 'e') }} -FROM {{ source[0] }} AS e -{% if is_incremental() -%} -LEFT JOIN ( - SELECT {{ dbtvault.prefix(tgt_cols_list, 'd') }} - FROM ( - SELECT {{ dbtvault.prefix(tgt_cols_list, 'c') }}, - CASE WHEN RANK() - OVER (PARTITION BY {{ dbtvault.prefix([tgt_pk|last], 'c') }} - ORDER BY {{ dbtvault.prefix([tgt_ldts|last], 'c') }} DESC) = 1 - THEN 'Y' ELSE 'N' END CURR_FLG - FROM ( - SELECT {{ dbtvault.prefix(tgt_cols_list, 'a') }} - FROM {{ this }} as a - JOIN {{ source[0] }} as b - ON {{ dbtvault.prefix([tgt_pk|last], 'a') }} = {{ dbtvault.prefix([src_pk], 'b') }} - ) as c - ) AS d -WHERE d.CURR_FLG = 'Y') AS src -ON {{ dbtvault.prefix([tgt_hashdiff|last], 'src') }} = {{ dbtvault.prefix([src_hashdiff], 'e') }} -WHERE {{ dbtvault.prefix([tgt_hashdiff|last], 'src') }} IS NULL -{%- endif -%} - -{% endmacro %} diff --git a/macros/tables/t_link_template.sql b/macros/tables/t_link_template.sql deleted file mode 100644 index b8c1d49e3..000000000 --- a/macros/tables/t_link_template.sql +++ /dev/null @@ -1,45 +0,0 @@ -{#- Copyright 2019 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} -{%- macro t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, - source) -%} - -{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_fk=src_fk, src_payload=src_payload, - src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, - tgt_pk=tgt_pk, tgt_fk=tgt_fk, tgt_payload=tgt_payload, - tgt_eff=tgt_eff, tgt_ldts=tgt_ldts, tgt_source=tgt_source, - source=source) -%} - -{%- set tgt_pk = tgt_cols['tgt_pk'] -%} -{%- set tgt_fk = tgt_cols['tgt_fk'] -%} -{%- set tgt_payload = tgt_cols['tgt_payload'] -%} -{%- set tgt_eff = tgt_cols['tgt_eff'] -%} -{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} -{%- set tgt_source = tgt_cols['tgt_source'] -%} - -{%- set is_union = dbtvault.is_union(source) -%} --- Generated by dbtvault. Copyright 2019 Business Thinking LTD. trading as Datavault -SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source], 'stg') }} -FROM ( - SELECT {{ dbtvault.prefix([src_pk, src_fk, src_payload, src_eff, - src_ldts, src_source], 'stg') }} - FROM {{ source[0] }} AS stg -) AS stg -{% if is_incremental() -%} -LEFT JOIN {{ this }} AS tgt -ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} -WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL -{%- endif -%} -{%- endmacro -%} \ No newline at end of file From 0a2c96dc3b8eeb2b0b2200ee42f1dfb66084f2b3 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 25 Mar 2020 10:01:51 +0000 Subject: [PATCH 110/157] Added sfk and dfk parameter doc --- docs/macros.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/macros.md b/docs/macros.md index 9034356ba..525784bef 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -303,8 +303,8 @@ Generates sql to build a effectivity satellite table using the provided metadata | Parameter | Description | Type | Required? | | -------------- | -------------------------------------------------------- | -------------- | ------------------------------------------------------------------ | | src_pk | Source primary key column | String | check_circle | -| src_dfk | Coming soon. | String | check_circle | -| src_sfk | Coming soon. | String | check_circle | +| src_dfk | Source driving foreign key | String | check_circle | +| src_sfk | Source secondary foreign key | String | check_circle | | src_ldts | Source loaddate timestamp column | String | check_circle | | src_eff_from | Source effective from column | String | check_circle | | src_start_date | The date which a link record is open/closed from | String | check_circle | From f36cd9394d7487db6aaf6d6ce13f207b44b38792 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 25 Mar 2020 12:06:49 +0000 Subject: [PATCH 111/157] Updated sign up link --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d6c269704..cf5eb77a9 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,9 @@ And run ## Sign up for early-bird announcements -[SIGN UP](https://www.data-vault.co.uk/dbtvault/) and get notified of new features and new releases -before anyone else! +[![Sign up](https://img.shields.io/badge/Email-Sign--up-blue)](https://www.data-vault.co.uk/dbtvault/) + +Get notified of new features and new releases before anyone else! ## Contributing [View our contribution guidelines](CONTRIBUTING.md) From fbbd62c47e8695460b2d84356b32d3d98bf00cec Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 14:25:08 +0100 Subject: [PATCH 112/157] Version 0.6 beta 2 THIS IS A BETA RELEASE, USE WITH CARE - Improvements to effectivity satellites - Added more documentation for effectivity satellites - Fixed more macro headers (removed copyrgith etc.) - Improved Link macro to work with effectivity satellites --- docs/changelog.md | 7 + docs/eff_sats.md | 154 +++++++++++++++ docs/loading.md | 33 ++++ docs/macros.md | 182 ++++++++++++++++-- macros/internal/check_relation.sql | 5 +- macros/internal/get_src_col_list.sql | 6 +- macros/internal/hash_check.sql | 24 +++ macros/internal/is_multi_source.sql | 4 +- macros/internal/is_union.sql | 5 +- macros/internal/multikey.sql | 52 +++++ macros/internal/new_union.sql | 5 +- macros/internal/retrieve_tgt_cols.sql | 5 +- macros/internal/single.sql | 5 +- macros/internal/source_columns.sql | 5 +- macros/internal/validate_columns.sql | 5 +- macros/internal_deprecated/create_source.sql | 5 +- .../internal_deprecated/create_tgt_cols.sql | 5 +- macros/internal_deprecated/get_col_list.sql | 5 +- macros/internal_deprecated/union.sql | 5 +- macros/staging/add_columns.sql | 5 +- macros/staging/from.sql | 5 +- macros/staging/multi_hash.sql | 7 +- macros/supporting/cast.sql | 5 +- macros/supporting/hash.sql | 5 +- macros/supporting/prefix.sql | 5 +- macros/tables/eff_sat.sql | 62 ++++-- macros/tables/hub.sql | 1 + macros/tables/link.sql | 23 ++- macros/tables/sat.sql | 1 + macros/tables/t_link.sql | 1 + macros/tables_deprecated/hub_template.sql | 7 +- macros/tables_deprecated/link_template.sql | 7 +- macros/tables_deprecated/sat_template.sql | 7 +- macros/tables_deprecated/t_link_template.sql | 7 +- 34 files changed, 557 insertions(+), 108 deletions(-) create mode 100644 docs/eff_sats.md create mode 100644 macros/internal/hash_check.sql create mode 100644 macros/internal/multikey.sql diff --git a/docs/changelog.md b/docs/changelog.md index be5f3821b..d58c50b35 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.6] - 2020-04-13 +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.6)](https://dbtvault.readthedocs.io/en/v0.5/?badge=v0.6) + +### Added + +- The [eff_sat](macros.md#eff_sat) macro. + ## [v0.5] - 2020-02-24 [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.5)](https://dbtvault.readthedocs.io/en/v0.5/?badge=v0.5) diff --git a/docs/eff_sats.md b/docs/eff_sats.md new file mode 100644 index 000000000..35faa6ede --- /dev/null +++ b/docs/eff_sats.md @@ -0,0 +1,154 @@ +Effectivity satellites are used on links to provide data on which links are currently active and those that are not. + +Effectivity satellites contain the following columns: + +1. A primary key, which is also the primary key of the [link](macros.md#link). This is the natural keys (prior to hashing) represented by the foreign key columns +and create a hash on a concatenation of them. + +2. Effective From date or date timestamp. This is the date that the record is effective from. + +3. Start date or date timestamp. This is the date from which the link record is currently the latest/active link. + +4. End date or date timestamp. This is the date that a link is made inactive. If it is an active link, a record will +contain a max date of ```9999-12-31```. + +5. The load date or load date timestamp. + +6. The source for the record. + +### Creating the model header + +Create another empty dbt model. We'll call this one ```eff_sat_customer_order.sql``` + +The following header is what we use, but feel free to customise it to your needs: + +```eff_sat_customer_order.sql``` +```sql +{{- config(materialized='incremental', schema='MYSCHEMA', tags='eff_sat') -}} + +``` + +### Adding the metadata + +Now we need to provide some metadata to the [eff_sat](macros.md#eff_sat) macro. + +#### Source table + +The first pieces of metadata we need is the source table and the link table the effectivity satellite is hanging off. +This step is easy, as we created the staging layer ourselves. All we need to do is provide the name of the staging +layer in the ```dbt_project.yml``` file under ```source``` and the link table name under ```link``` +and dbtvault will do the rest for us. + +```dbt_project.yml``` + +```yaml +eff_sat_customer_order: + vars: + source: 'stg_customer_order_hashed' + link: 'link_customer_order' + ... +``` + +#### Source columns + +Next, we define the columns which we would like to bring from the source. +Using our knowledge of what columns we need in our ```eff_sat_customer_order``` table and the columns required help +compute the effectivity satellite logic (all required columns can be found in the [eff_sat](macros.md#eff_sat)). We can +identify columns in our staging layer which we will then use to form our effectivity satellite: + +1. A primary key, which is a combination of the two natural keys: In this case ```CUSTOMER_ORDER_PK``` +which we added in our staging layer. +2. ```CUSTOMER_FK``` which is one of the foreign keys in the link. This is the foreign key that is going to be used as the +driving foreign key. +3. ```ORDER_FK``` which is the other foreign key in the link. This is the foreign key that is going to be used as the +secondary foreign key. +4. ```EFFECTIVE_FROM``` which is the date in the staging table that states when a record becomes effective. +5.```START_DATETIME``` which is the column in the effectivity satellite whose date defines when a link record begins its +activity. +6. ```END_DATETIME``` which is the column in the effectivity satellite whose date defines when a link record ends its +activity and becomes inactive. Active link records will have a date equal to the max date ```9999-12-31```. +7. A load date timestamp, which is present in the staging layer as ```LOADDATE``` +8. A ```SOURCE``` column. + +We can now add this metadata to the ```dbt_project.yml``` file: + +```dbt_project.yml``` +```yaml hl_lines="5 6 7 8 9 10 11 12" +eff_sat_customer_order: + vars: + source: 'stg_customer_order_hashed' + link: 'link_customer_order' + src_pk: 'CUSTOMER_ORDER_PK' + src_dfk: 'CUSTOMER_FK' + src_sfk: 'ORDER_FK' + src_eff_from: 'EFFECTIVE_FROM' + src_start_date: 'START_DATETIME' + src_end_date: 'END_DATETIME' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' +``` + +!!! note + Links can often contain more than two foreign keys. For cases where a link contains more than two foreign keys, + please see the multi-part key section below. + +### Invoking the template + +Now we bring it all together and call the [eff_sat](macros.md#eff_sat) macro: + +```eff_sat_customer_order.sql``` +``` sql hl_lines="2 3 4 5" +{{- config(materialized='incremental', schema='MYSCHEMA', tags='eff_sat') -}} +-- depends_on: {{ ref(var('link')) }} +{{ dbtvault.eff_sat(var('src_pk'), var('src_dfk'), var('src_sfk'), var('src_ldts'), + var('src_eff_from'), var('src_start_date'), var('src_end_date'), + var('src_source'), var('link'), var('source')) }} +``` + +!!! note + Unlike the other macros, line 2 of this example is required only for the effectivity satellite. For more info on + this please see the [eff_sat](macros.md#eff_sat) macro documentation. + +### Running dbt + +With our model complete, we can run dbt to create our ```eff_sat_customer_order``` effectivity satellite. + +```dbt run --models +eff_sat_customer_order``` + +And our table will look like this: + +| CUSTOMER_ORDER_PK | EFFECTIVE_FROM | START_DATETIME | END_DATETIME | LOADDATE | SOURCE | +| ----------------- | -------------- | -------------- | ------------ | ---------- | ------------ | +| 72A160... | 1993-01-01 | 1993-01-01 | 9999-12-31 | 1993-01-01 | 1 | +| . | . | . | . | . | . | +| . | . | . | . | . | . | +| 1CE6A9... | 1993-01-01 | 1993-01-01 | 9999-12-31 | 1993-01-01 | 1 | + + +### Multi-part foreign keys + +In some cases, a link may contain more than two foreign keys. The [eff_sat](macros.md#eff_sat) macro accounts for this +by accepting a multi-part driving foreign key and a multi-part secondary foreign key. To supply this metadata to the +macro, the driving foreign keys and secondary foreign keys column names must be supplied as a list. An example of this +can be seen below: + +```dbt_project.yml``` +```yaml hl_lines="6 7 8 9 10 11 12" +eff_sat_customer_order: + vars: + source: 'stg_customer_order_hashed' + link: 'link_customer_order' + src_pk: 'CUSTOMER_ORDER_PK' + src_dfk: + - 'CUSTOMER_FK' + - 'NATION_FK' + src_sfk: + - 'ORDER_FK' + - 'PRODUCT_FK' + - 'ORGANISATION_FK' + src_eff_from: 'EFFECTIVE_FROM' + src_start_date: 'START_DATETIME' + src_end_date: 'END_DATETIME' + src_ldts: 'LOADDATE' + src_source: 'SOURCE' +``` diff --git a/docs/loading.md b/docs/loading.md index 000ae774f..a6be2006d 100644 --- a/docs/loading.md +++ b/docs/loading.md @@ -115,6 +115,39 @@ To compile and load the provided t_link models, run the following command: This will run all models with the t_link tag. +## Effectivity Satellites + +!!! note "Work in progress." + The [eff_sat](macros.md#eff_sat) macro is currently unreleased and we are working on providing a proper worked + example to demonstrate this macro's full capabilities. + +An effectivity satellite is a type satellite that hangs off a link and records the effectivity of a link record with a +start and end date for the period of effectivity. + +Our effectivity satellite will contain: + +1. A primary key, which is a combination of the two natural keys: In this case ```CUSTOMER_ORDER_PK``` +which we added in our staging layer. +2. ```CUSTOMER_FK``` which is one of the foreign keys in the link. This is the foreign key that is going to be used as the +driving foreign key. +3. ```ORDER_FK``` which is the other foreign key in the link. This is the foreign key that is going to be used as the +secondary foreign key. +4. ```EFFECTIVE_FROM``` which is the date in the staging table that states when a record becomes effective. +5.```START_DATETIME``` which is the column in the effectivity satellite whose date defines when a link record begins its +activity. +6. ```END_DATETIME``` which is the column in the effectivity satellite whose date defines when a link record ends its +activity and becomes inactive. Active link records will have a date equal to the max date ```9999-12-31```. +7. A load date timestamp, which is present in the staging layer as ```LOADDATE``` +8. A ```SOURCE``` column. + +### Loading effectivity satellites + +To compile and load the provided effectivity satellite models, run the following command: + +```dbt run --models tag:eff_sat``` + +This will run all models with the eff_sat tag. + ## Loading the full system Each of the commands above load a particular type of table, however, we may want to do a full system load. diff --git a/docs/macros.md b/docs/macros.md index 525784bef..470b73875 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -300,27 +300,181 @@ Generates sql to build a effectivity satellite table using the provided metadata #### Parameters -| Parameter | Description | Type | Required? | -| -------------- | -------------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_dfk | Source driving foreign key | String | check_circle | -| src_sfk | Source secondary foreign key | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_eff_from | Source effective from column | String | check_circle | -| src_start_date | The date which a link record is open/closed from | String | check_circle | -| src_end_date | The date which a link record is open/closed to | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| link | The link which this effectivity satellite is attached to | String | check_circle | -| source | Staging model reference or table name | String | check_circle | | | check_circle | +| Parameter | Description | Type (Single-part keys) | Type (Multi-part keys) | Required? | +| -------------- | -------------------------------------------------------- | ----------------------- | ----------------------- | ------------------------------------------------------------------ | +| src_pk | Source primary key column | String | String | check_circle | +| src_dfk | Source driving foreign key column | String | String/List (YAML) | check_circle | +| src_sfk | Source secondary foreign key column | String | String/List (YAML) | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_eff_from | Source effective from column | String | String | check_circle | +| src_start_date | The date which a link record is open/closed from | String | String | check_circle | +| src_end_date | The date which a link record is open/closed to | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| link | The link which this effectivity satellite is attached to | String | String | check_circle | +| source | Staging model reference or table name | String | String | check_circle | | | check_circle | #### Usage -Coming soon. +```mysql +{{- config(...) -}} +-- depends_on: {{ ref(var('link')) }} +{{ dbtvault.eff_sat(var('src_pk'), var('src_dfk'), var('src_sfk'), var('src_ldts'), + var('src_eff_from'), var('src_start_date'), var('src_end_date'), + var('src_source'), var('link'), var('source')) }} +``` + +!!! note + As you can see, currently for the usage of the eff_sat macro we have the extra line of code + ```-- depends_on: {{ ref(var('link')) }}```. This is due to the structure of dependencies in dbt. Another method is + being investigated but this fix currently passes all the our tests. #### Example output -Coming soon. +Here are some example outputs for the incremental steps of effectivity satellite models. + +```mysql tab='Single-part key' +WITH +c AS ( + SELECT DISTINCT + a.CUSTOMER_ORDER_PK, a.LOADDATE, a.EFFECTIVE_FROM, a.START_DATETIME, a.END_DATETIME, a.SOURCE + FROM DBT_VAULT.TEST_vlt.test_eff_customer_order_current AS a + INNER JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS b ON a.CUSTOMER_ORDER_PK=b.CUSTOMER_ORDER_PK + ) +, d as ( + SELECT + c.CUSTOMER_ORDER_PK, c.LOADDATE, c.EFFECTIVE_FROM, c.START_DATETIME, c.END_DATETIME, c.SOURCE, + CASE WHEN RANK() + OVER (PARTITION BY c.CUSTOMER_ORDER_PK + ORDER BY c.END_DATETIME ASC) = 1 + THEN 'Y' ELSE 'N' END AS CURR_FLG + FROM c + ) +, p AS ( + SELECT q.* FROM DBT_VAULT.TEST_vlt.test_link_customer_order_current AS q + INNER JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS r ON q.CUSTOMER_FK=r.CUSTOMER_FK + ) +, x AS ( + SELECT p.* + , s.CUSTOMER_FK AS DFK_1 + FROM p + LEFT JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS s ON p.CUSTOMER_FK=s.CUSTOMER_FK + AND p.ORDER_FK=s.ORDER_FK + WHERE (s.CUSTOMER_FK IS NULL AND s.ORDER_FK IS NULL) + ) +, y AS ( + SELECT + t.CUSTOMER_ORDER_PK, t.LOADDATE, t.SOURCE, t.EFFECTIVE_FROM, t.START_DATETIME, t.END_DATETIME + , x.DFK_1 + , x.CUSTOMER_FK, + CASE WHEN RANK() + OVER (PARTITION BY t.CUSTOMER_ORDER_PK + ORDER BY t.END_DATETIME ASC) = 1 + THEN 'Y' ELSE 'N' END AS CURR_FLG + FROM x + INNER JOIN DBT_VAULT.TEST_vlt.test_eff_customer_order_current AS t ON x.CUSTOMER_ORDER_PK=t.CUSTOMER_ORDER_PK + ) + +SELECT DISTINCT + e.CUSTOMER_ORDER_PK, e.LOADDATE, e.SOURCE, e.EFFECTIVE_FROM, + e.EFFECTIVE_FROM AS START_DATETIME, + e.END_DATETIME +FROM DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS e +LEFT JOIN ( + SELECT d.CUSTOMER_ORDER_PK, d.LOADDATE, d.EFFECTIVE_FROM, d.START_DATETIME, d.END_DATETIME, d.SOURCE + FROM d + WHERE d.CURR_FLG = 'Y' AND d.END_DATETIME=TO_DATE('9999-12-31') + ) AS eff +ON eff.CUSTOMER_ORDER_PK=e.CUSTOMER_ORDER_PK +WHERE (eff.CUSTOMER_ORDER_PK IS NULL +AND e.ORDER_FK<>MD5_BINARY('^^') AND e.CUSTOMER_FK<>MD5_BINARY('^^')) +UNION +SELECT + y.CUSTOMER_ORDER_PK, + z.LOADDATE, + y.SOURCE, y.EFFECTIVE_FROM, y.START_DATETIME, + CASE WHEN + y.DFK_1 IS NULL + THEN z.EFFECTIVE_FROM ELSE '9999-12-31' END AS END_DATETIME +FROM y +LEFT JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS z ON y.CUSTOMER_FK=z.CUSTOMER_FK +WHERE (y.CURR_FLG='Y' AND y.END_DATETIME='9999-12-31') + +``` + +```mysql tab='Multi-part key' +WITH +c AS ( + SELECT DISTINCT + a.CUSTOMER_ORDER_PK, a.LOADDATE, a.EFFECTIVE_FROM, a.START_DATETIME, a.END_DATETIME, a.SOURCE + FROM DBT_VAULT.TEST_vlt.test_eff_customer_order_multipart_current AS a + INNER JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS b ON a.CUSTOMER_ORDER_PK=b.CUSTOMER_ORDER_PK + ) +, d as ( + SELECT + c.CUSTOMER_ORDER_PK, c.LOADDATE, c.EFFECTIVE_FROM, c.START_DATETIME, c.END_DATETIME, c.SOURCE, + CASE WHEN RANK() + OVER (PARTITION BY c.CUSTOMER_ORDER_PK + ORDER BY c.END_DATETIME ASC) = 1 + THEN 'Y' ELSE 'N' END AS CURR_FLG + FROM c + ) +, p AS ( + SELECT q.* FROM DBT_VAULT.TEST_vlt.test_link_customer_order_multipart_current AS q + INNER JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS r ON q.CUSTOMER_FK=r.CUSTOMER_FK + AND q.NATION_FK=r.NATION_FK + ) +, x AS ( + SELECT p.* + , s.CUSTOMER_FK AS DFK_1 + , s.NATION_FK AS DFK_2 + FROM p + LEFT JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS s ON p.CUSTOMER_FK=s.CUSTOMER_FK AND p.NATION_FK=s.NATION_FK + AND p.ORDER_FK=s.ORDER_FK AND p.PRODUCT_FK=s.PRODUCT_FK AND p.ORGANISATION_FK=s.ORGANISATION_FK + WHERE (s.CUSTOMER_FK IS NULL AND s.NATION_FK IS NULL + AND s.ORDER_FK IS NULL AND s.PRODUCT_FK IS NULL AND s.ORGANISATION_FK IS NULL) + ) +, y AS ( + SELECT + t.CUSTOMER_ORDER_PK, t.LOADDATE, t.SOURCE, t.EFFECTIVE_FROM, t.START_DATETIME, t.END_DATETIME + , x.DFK_1 + , x.DFK_2 + , x.CUSTOMER_FK + , x.NATION_FK, + CASE WHEN RANK() + OVER (PARTITION BY t.CUSTOMER_ORDER_PK + ORDER BY t.END_DATETIME ASC) = 1 + THEN 'Y' ELSE 'N' END AS CURR_FLG + FROM x + INNER JOIN DBT_VAULT.TEST_vlt.test_eff_customer_order_multipart_current AS t ON x.CUSTOMER_ORDER_PK=t.CUSTOMER_ORDER_PK + ) + +SELECT DISTINCT + e.CUSTOMER_ORDER_PK, e.LOADDATE, e.SOURCE, e.EFFECTIVE_FROM, + e.EFFECTIVE_FROM AS START_DATETIME, + e.END_DATETIME +FROM DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS e +LEFT JOIN ( + SELECT d.CUSTOMER_ORDER_PK, d.LOADDATE, d.EFFECTIVE_FROM, d.START_DATETIME, d.END_DATETIME, d.SOURCE + FROM d + WHERE d.CURR_FLG = 'Y' AND d.END_DATETIME=TO_DATE('9999-12-31') + ) AS eff +ON eff.CUSTOMER_ORDER_PK=e.CUSTOMER_ORDER_PK +WHERE (eff.CUSTOMER_ORDER_PK IS NULL AND e.ORDER_FK<>MD5_BINARY('^^') AND e.PRODUCT_FK<>MD5_BINARY('^^') + AND e.ORGANISATION_FK<>MD5_BINARY('^^') AND e.CUSTOMER_FK<>MD5_BINARY('^^') AND e.NATION_FK<>MD5_BINARY('^^')) +UNION +SELECT + y.CUSTOMER_ORDER_PK, + z.LOADDATE, + y.SOURCE, y.EFFECTIVE_FROM, y.START_DATETIME, + CASE WHEN + y.DFK_1 IS NULL + AND y.DFK_2 IS NULL + THEN z.EFFECTIVE_FROM ELSE '9999-12-31' END AS END_DATETIME +FROM y +LEFT JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS z ON y.CUSTOMER_FK=z.CUSTOMER_FK AND y.NATION_FK=z.NATION_FK +WHERE (y.CURR_FLG='Y' AND y.END_DATETIME='9999-12-31') +``` ___ diff --git a/macros/internal/check_relation.sql b/macros/internal/check_relation.sql index 88c398e93..2f2a8b16f 100644 --- a/macros/internal/check_relation.sql +++ b/macros/internal/check_relation.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro check_relation(obj) -%} {%- if not (obj is mapping and obj.get('metadata', {}).get('type', '').endswith('Relation')) -%} diff --git a/macros/internal/get_src_col_list.sql b/macros/internal/get_src_col_list.sql index 1abda8ac7..c38908ecd 100644 --- a/macros/internal/get_src_col_list.sql +++ b/macros/internal/get_src_col_list.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,8 +10,8 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro get_src_col_list(tgt_cols) -%} +{%- macro get_src_col_list(tgt_cols) -%} {%- set col_list = [] -%} diff --git a/macros/internal/hash_check.sql b/macros/internal/hash_check.sql new file mode 100644 index 000000000..335e82354 --- /dev/null +++ b/macros/internal/hash_check.sql @@ -0,0 +1,24 @@ +{#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} + +{%- macro hash_check(hash) -%} + +{%- if hash == 'MD5' %} +MD5_BINARY('^^') +{%- elif hash == 'SHA' %} +SHA2_BINARY('^^') +{%- else %} +MD5_BINARY('^^') +{% endif %} + +{% endmacro %} \ No newline at end of file diff --git a/macros/internal/is_multi_source.sql b/macros/internal/is_multi_source.sql index c31fb870e..0e836af27 100644 --- a/macros/internal/is_multi_source.sql +++ b/macros/internal/is_multi_source.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at diff --git a/macros/internal/is_union.sql b/macros/internal/is_union.sql index b87045145..cfd004d84 100644 --- a/macros/internal/is_union.sql +++ b/macros/internal/is_union.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro is_union(obj) -%} {%- if obj is iterable and obj is not string -%} diff --git a/macros/internal/multikey.sql b/macros/internal/multikey.sql new file mode 100644 index 000000000..5d181c519 --- /dev/null +++ b/macros/internal/multikey.sql @@ -0,0 +1,52 @@ +{#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} + +{%- macro multikey(columns, aliases, type_for) -%} + +{% if type_for == 'join' %} + +{% for col in columns if not columns is string %} + {% if loop.index == columns|length %} + {{ dbtvault.prefix([col], aliases[0]) }}={{ dbtvault.prefix([col], aliases[1]) }} + {% else %} + {{ dbtvault.prefix([col], aliases[0]) }}={{ dbtvault.prefix([col], aliases[1]) }} AND + {% endif %} +{% else %} + {{ dbtvault.prefix([columns], aliases[0]) }}={{ dbtvault.prefix([columns], aliases[1]) }} +{% endfor %} + +{% elif type_for == 'where null'%} + +{% for col in columns if not columns is string %} + {% if loop.index == columns|length %} + {{ dbtvault.prefix([col], aliases[0]) }} IS NULL + {% else %} + {{ dbtvault.prefix([col], aliases[0]) }} IS NULL AND + {% endif %} +{% else %} + {{ dbtvault.prefix([columns], aliases[0]) }} IS NULL +{% endfor %} + +{% elif type_for == 'where not null'%} + +{% for col in columns if not columns is string %} + {% if loop.index == columns|length %} + {{ dbtvault.prefix([col], aliases[0]) }}<>{{ dbtvault.hash_check(var('hash')) }} + {% else %} + {{ dbtvault.prefix([col], aliases[0]) }}<>{{ dbtvault.hash_check(var('hash')) }} AND + {% endif %} +{% else %} + {{ dbtvault.prefix([columns], aliases[0]) }}<>{{ dbtvault.hash_check(var('hash')) }} +{% endfor %} +{% endif %} +{% endmacro %} \ No newline at end of file diff --git a/macros/internal/new_union.sql b/macros/internal/new_union.sql index cf8c53a74..8d8fd4144 100644 --- a/macros/internal/new_union.sql +++ b/macros/internal/new_union.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro new_union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -%} SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'src')}}, diff --git a/macros/internal/retrieve_tgt_cols.sql b/macros/internal/retrieve_tgt_cols.sql index 5614cb556..fa305b769 100644 --- a/macros/internal/retrieve_tgt_cols.sql +++ b/macros/internal/retrieve_tgt_cols.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro retrieve_tgt_cols() -%} {%- set tgt_pk = [ ref(kwargs['tgt_pk']|default(None, true)) ] -%} diff --git a/macros/internal/single.sql b/macros/internal/single.sql index 00b106a64..95e3cf2c4 100644 --- a/macros/internal/single.sql +++ b/macros/internal/single.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro single(src_pk, src_nk, src_ldts, src_source, source, letter='a') -%} diff --git a/macros/internal/source_columns.sql b/macros/internal/source_columns.sql index bf64892cf..f4dc52d6c 100644 --- a/macros/internal/source_columns.sql +++ b/macros/internal/source_columns.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro source_columns(src_pk, src_nk, src_ldts, src_source, source, is_union) -%} diff --git a/macros/internal/validate_columns.sql b/macros/internal/validate_columns.sql index 4dea7c1aa..3d133e71a 100644 --- a/macros/internal/validate_columns.sql +++ b/macros/internal/validate_columns.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro validate_columns(select_columns, source_columns, source_relation) -%} {%- if source_columns -%} diff --git a/macros/internal_deprecated/create_source.sql b/macros/internal_deprecated/create_source.sql index 29dc10e31..8014b5681 100644 --- a/macros/internal_deprecated/create_source.sql +++ b/macros/internal_deprecated/create_source.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro create_source(src_pk, src_nk, src_ldts, src_source, tgt_pk, tgt_nk, tgt_ldts, tgt_source, source, is_union) -%} diff --git a/macros/internal_deprecated/create_tgt_cols.sql b/macros/internal_deprecated/create_tgt_cols.sql index 658aeedd0..75358cf0a 100644 --- a/macros/internal_deprecated/create_tgt_cols.sql +++ b/macros/internal_deprecated/create_tgt_cols.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro create_tgt_cols() -%} {%- set tgt_pk = kwargs['tgt_pk']|default(None, true) -%} diff --git a/macros/internal_deprecated/get_col_list.sql b/macros/internal_deprecated/get_col_list.sql index 320eaa0b4..c1dc53a04 100644 --- a/macros/internal_deprecated/get_col_list.sql +++ b/macros/internal_deprecated/get_col_list.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro get_col_list(tgt_cols) -%} diff --git a/macros/internal_deprecated/union.sql b/macros/internal_deprecated/union.sql index 78ab0cb5f..45f1290b4 100644 --- a/macros/internal_deprecated/union.sql +++ b/macros/internal_deprecated/union.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -%} SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'src')}}, diff --git a/macros/staging/add_columns.sql b/macros/staging/add_columns.sql index 97317ef67..14593889d 100644 --- a/macros/staging/add_columns.sql +++ b/macros/staging/add_columns.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro add_columns(source, pairs=[]) -%} {%- set exclude_columns = [] -%} diff --git a/macros/staging/from.sql b/macros/staging/from.sql index fac6a9be9..3b9608d79 100644 --- a/macros/staging/from.sql +++ b/macros/staging/from.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {% macro from(source_table) %} FROM {{ source_table }} diff --git a/macros/staging/multi_hash.sql b/macros/staging/multi_hash.sql index 3100e8112..06c8ce17b 100644 --- a/macros/staging/multi_hash.sql +++ b/macros/staging/multi_hash.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,8 +10,9 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro multi_hash(triples) -%} --- Generated by dbtvault. Copyright 2020 Business Thinking LTD. trading as Datavault +-- Generated by dbtvault. SELECT {% for triple in triples -%} {%- if triple | length == 2 -%} diff --git a/macros/supporting/cast.sql b/macros/supporting/cast.sql index fad2b3cdf..9e3e02abd 100644 --- a/macros/supporting/cast.sql +++ b/macros/supporting/cast.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro cast(columns, prefix=none) -%} {#- If a string or list -#} diff --git a/macros/supporting/hash.sql b/macros/supporting/hash.sql index 2d069b72f..a60239df5 100644 --- a/macros/supporting/hash.sql +++ b/macros/supporting/hash.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro hash(columns, alias, sort=false) -%} {%- set hash = var('hash', 'MD5') -%} diff --git a/macros/supporting/prefix.sql b/macros/supporting/prefix.sql index 4d8c37d40..d8e5d3ee2 100644 --- a/macros/supporting/prefix.sql +++ b/macros/supporting/prefix.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro prefix(columns, prefix_str) -%} {%- for column in columns -%} diff --git a/macros/tables/eff_sat.sql b/macros/tables/eff_sat.sql index e035c9ee5..eb4ab9d0f 100644 --- a/macros/tables/eff_sat.sql +++ b/macros/tables/eff_sat.sql @@ -10,11 +10,13 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro eff_sat(src_pk, src_dfk, src_sfk, src_ldts, src_eff_from, src_start_date, src_end_date, src_source, link, source)-%} {%- set source_cols = dbtvault.get_src_col_list([src_pk, src_ldts, src_eff_from, src_start_date, src_end_date, src_source])-%} {%- set max_date = "'" ~ '9999-12-31' ~ "'" -%} - +-- Generated by dbtvault. +{% if is_incremental() %} WITH {#- Reduce data set to size of stage table. #} c AS (SELECT DISTINCT @@ -32,20 +34,36 @@ c AS (SELECT DISTINCT FROM c) , p AS ( SELECT q.* FROM {{ ref(link) }} AS q - INNER JOIN {{ ref(source) }} AS r ON {{ dbtvault.prefix([src_dfk], 'q') }}={{ dbtvault.prefix([src_dfk], 'r') }} + INNER JOIN {{ ref(source) }} AS r ON + {{ dbtvault.multikey(src_dfk, ['q', 'r'], 'join') }} ) , x AS ( - SELECT p.*, {{ dbtvault.prefix([src_dfk], 's') }} AS STG_CUSTOMER_FK + SELECT p.* + {% for dfk in src_dfk if not src_dfk is string %} + , {{ dbtvault.prefix([dfk], 's') }} AS DFK_{{ loop.index }} + {% else %} + , {{ dbtvault.prefix([src_dfk], 's') }} AS DFK_1 + {% endfor %} FROM p - LEFT JOIN {{ ref(source) }} AS s ON {{ dbtvault.prefix([src_dfk], 'p') }}={{ dbtvault.prefix([src_dfk], 's') }} - AND {{ dbtvault.prefix([src_sfk], 'p') }}={{ dbtvault.prefix([src_sfk], 's') }} - WHERE ({{ dbtvault.prefix([src_dfk], 's') }} IS NULL AND {{ dbtvault.prefix([src_sfk], 's') }} IS NULL) + LEFT JOIN {{ ref(source) }} AS s ON + {{ dbtvault.multikey(src_dfk, ['p', 's'], 'join') }} + AND + {{ dbtvault.multikey(src_sfk, ['p', 's'], 'join') }} + WHERE ( + {{ dbtvault.multikey(src_dfk, ['s'], 'where null') }} + AND + {{ dbtvault.multikey(src_sfk, ['s'], 'where null') }} + ) ) , y AS ( SELECT - {{ dbtvault.prefix([src_pk, src_ldts, src_source, src_eff_from, src_start_date, src_end_date], 't') }}, - {{ dbtvault.prefix(['STG_CUSTOMER_FK'], 'x') }}, - {{ dbtvault.prefix([src_dfk], 'x')}}, + {{ dbtvault.prefix([src_pk, src_ldts, src_source, src_eff_from, src_start_date, src_end_date], 't') }} + {% for dfk in src_dfk if not src_dfk is string %} + , {{ dbtvault.prefix(['DFK_'~loop.index ], 'x') }} + {% else %} + , {{ dbtvault.prefix(['DFK_1'], 'x') }} + {% endfor %} + , {{ dbtvault.prefix([src_dfk], 'x')}}, CASE WHEN RANK() OVER (PARTITION BY {{ dbtvault.prefix([src_pk], 't') }} ORDER BY {{ dbtvault.prefix([src_end_date], 't') }} ASC) = 1 @@ -53,7 +71,7 @@ c AS (SELECT DISTINCT FROM x INNER JOIN {{ this }} AS t ON {{ dbtvault.prefix([src_pk], 'x') }}={{ dbtvault.prefix([src_pk], 't') }} ) - +{% endif %} SELECT DISTINCT {{ dbtvault.prefix([src_pk, src_ldts, src_source, src_eff_from], 'e') }}, {{ dbtvault.prefix([src_eff_from], 'e') }} AS {{ src_start_date }}, @@ -66,17 +84,31 @@ LEFT JOIN ( WHERE d.CURR_FLG = 'Y' AND {{ dbtvault.prefix([src_end_date], 'd') }}=TO_DATE({{ max_date }}) ) AS eff ON {{ dbtvault.prefix([src_pk], 'eff') }}={{ dbtvault.prefix([src_pk], 'e') }} -WHERE {{ dbtvault.prefix([src_pk], 'eff') }} IS NULL +WHERE ({{ dbtvault.prefix([src_pk], 'eff') }} IS NULL +AND +{{ dbtvault.multikey(src_sfk, ['e'], 'where not null') }} +AND +{{ dbtvault.multikey(src_dfk, ['e'], 'where not null') }} +) UNION SELECT {{ dbtvault.prefix([src_pk], 'y') }}, {{ dbtvault.prefix([src_ldts], 'z') }}, {{ dbtvault.prefix([src_source, src_eff_from, src_start_date], 'y') }}, - CASE WHEN y.STG_CUSTOMER_FK IS NULL + CASE WHEN + {% for dfk in src_dfk if not src_dfk is string %} + {% if loop.index == src_dfk|length %} + y.DFK_{{loop.index|string}} IS NULL + {% else %} + y.DFK_{{loop.index|string}} IS NULL AND + {% endif %} + {% else %} + y.DFK_1 IS NULL + {% endfor %} THEN {{ dbtvault.prefix([src_eff_from], 'z') }} ELSE {{ max_date }} END AS {{ src_end_date }} FROM y -LEFT JOIN {{ ref(source) }} AS z ON {{ dbtvault.prefix([src_dfk], 'y') }}={{ dbtvault.prefix([src_dfk], 'z') }} -WHERE y.CURR_FLG='Y' AND {{ dbtvault.prefix([src_end_date], 'y') }}={{ max_date }} +LEFT JOIN {{ ref(source) }} AS z ON +{{ dbtvault.multikey(src_dfk, ['y', 'z'], 'join') }} +WHERE (y.CURR_FLG='Y' AND {{ dbtvault.prefix([src_end_date], 'y') }}={{ max_date }}) {%- endif -%} - {% endmacro %} \ No newline at end of file diff --git a/macros/tables/hub.sql b/macros/tables/hub.sql index cf3e8017b..634e10bcf 100644 --- a/macros/tables/hub.sql +++ b/macros/tables/hub.sql @@ -10,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro hub(src_pk, src_nk, src_ldts, src_source, source) -%} diff --git a/macros/tables/link.sql b/macros/tables/link.sql index 3014f44e0..357129aba 100644 --- a/macros/tables/link.sql +++ b/macros/tables/link.sql @@ -10,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro link(src_pk, src_fk, src_ldts, src_source, source) -%} @@ -27,13 +28,27 @@ FROM ( LEFT JOIN {{ this }} AS tgt ON {{ dbtvault.prefix([src_pk], 'stg') }} = {{ dbtvault.prefix([src_pk], 'tgt') }} WHERE {{ dbtvault.prefix([src_pk], 'tgt') }} IS NULL -{# If an incremental and union load -#} {% if is_union -%} AND stg.FIRST_SOURCE IS NULL {%- endif -%} -{%- endif -%} -{# If a union base-load #} -{%- if is_union and not is_incremental() -%} +{%- for fk in src_fk %} +AND {{ dbtvault.prefix([fk], 'stg') }}<>{{ dbtvault.hash_check(var('hash')) }} +{% endfor %} +{%- elif not is_incremental() -%} +{% if is_union %} WHERE stg.FIRST_SOURCE IS NULL +{%- for fk in src_fk %} +AND {{ dbtvault.prefix([fk], 'stg') }}<>{{ dbtvault.hash_check(var('hash')) }} +{% endfor %} +{% else %} +WHERE +{%- for fk in src_fk %} +{% if loop.index == src_fk|length %} +{{ dbtvault.prefix([fk], 'stg') }}<>{{ dbtvault.hash_check(var('hash')) }} +{% else %} +{{ dbtvault.prefix([fk], 'stg') }}<>{{ dbtvault.hash_check(var('hash')) }} AND +{% endif %} +{% endfor %} +{% endif %} {%- endif -%} {%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/sat.sql b/macros/tables/sat.sql index 6310d3484..fc6201fa4 100644 --- a/macros/tables/sat.sql +++ b/macros/tables/sat.sql @@ -10,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro sat(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, source) -%} diff --git a/macros/tables/t_link.sql b/macros/tables/t_link.sql index 5600b4990..8833a414c 100644 --- a/macros/tables/t_link.sql +++ b/macros/tables/t_link.sql @@ -10,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro t_link(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, source) -%} {%- set source_cols = dbtvault.get_src_col_list([src_pk, src_fk, src_payload, src_eff, src_ldts, src_source])-%} diff --git a/macros/tables_deprecated/hub_template.sql b/macros/tables_deprecated/hub_template.sql index 37bba42eb..e7a9b7060 100644 --- a/macros/tables_deprecated/hub_template.sql +++ b/macros/tables_deprecated/hub_template.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro hub_template(src_pk, src_nk, src_ldts, src_source, tgt_pk, tgt_nk, tgt_ldts, tgt_source, source) -%} @@ -26,7 +25,7 @@ {%- set tgt_source = tgt_cols['tgt_source'] -%} {%- set is_union = dbtvault.is_union(source) -%} --- Generated by dbtvault. Copyright 2020 Business Thinking LTD. trading as Datavault +-- Generated by dbtvault. SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_nk, tgt_ldts, tgt_source], 'stg') }} FROM ( {{ dbtvault.create_source(src_pk, src_nk, src_ldts, src_source, diff --git a/macros/tables_deprecated/link_template.sql b/macros/tables_deprecated/link_template.sql index bcc6b431a..6813d08f4 100644 --- a/macros/tables_deprecated/link_template.sql +++ b/macros/tables_deprecated/link_template.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro link_template(src_pk, src_fk, src_ldts, src_source, tgt_pk, tgt_fk, tgt_ldts, tgt_source, source) -%} @@ -26,7 +25,7 @@ {%- set tgt_source = tgt_cols['tgt_source'] -%} {%- set is_union = dbtvault.is_union(source) -%} --- Generated by dbtvault. Copyright 2020 Business Thinking LTD. trading as Datavault +-- Generated by dbtvault. SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_ldts, tgt_source], 'stg') }} FROM ( {{ dbtvault.create_source(src_pk, src_fk, src_ldts, src_source, diff --git a/macros/tables_deprecated/sat_template.sql b/macros/tables_deprecated/sat_template.sql index 9edb78c2b..a632e93bf 100644 --- a/macros/tables_deprecated/sat_template.sql +++ b/macros/tables_deprecated/sat_template.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro sat_template(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, tgt_pk, tgt_hashdiff, tgt_payload, @@ -35,7 +34,7 @@ {%- set tgt_cols_list = dbtvault.get_col_list([tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source]) -%} --- Generated by dbtvault. Copyright 2020 Business Thinking LTD. trading as Datavault +-- Generated by dbtvault. SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_hashdiff, tgt_payload, tgt_ldts, tgt_eff, tgt_source], 'e') }} FROM {{ source[0] }} AS e {% if is_incremental() -%} diff --git a/macros/tables_deprecated/t_link_template.sql b/macros/tables_deprecated/t_link_template.sql index 61e9f9ed4..e05b4cd2e 100644 --- a/macros/tables_deprecated/t_link_template.sql +++ b/macros/tables_deprecated/t_link_template.sql @@ -1,6 +1,4 @@ -{#- Copyright 2020 Business Thinking LTD. trading as Datavault - - Licensed under the Apache License, Version 2.0 (the "License"); +{#- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,6 +10,7 @@ See the License for the specific language governing permissions and limitations under the License. -#} + {%- macro t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, source) -%} @@ -30,7 +29,7 @@ {%- set tgt_source = tgt_cols['tgt_source'] -%} {%- set is_union = dbtvault.is_union(source) -%} --- Generated by dbtvault. Copyright 2020 Business Thinking LTD. trading as Datavault +-- Generated by dbtvault. SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source], 'stg') }} FROM ( SELECT {{ dbtvault.prefix([src_pk, src_fk, src_payload, src_eff, From 705401df26fe2980d5285d6c00a7a22f7bd6e11e Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 15:48:13 +0100 Subject: [PATCH 113/157] Fixed effectivity satellite page --- docs/eff_sats.md | 2 ++ mkdocs.yml | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/eff_sats.md b/docs/eff_sats.md index 35faa6ede..cdb0291ad 100644 --- a/docs/eff_sats.md +++ b/docs/eff_sats.md @@ -1,3 +1,5 @@ +# Effectivity satellites + Effectivity satellites are used on links to provide data on which links are currently active and those that are not. Effectivity satellites contain the following columns: diff --git a/mkdocs.yml b/mkdocs.yml index bdde1f389..1b736d3e0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,7 @@ nav: - Links: 'links.md' - Satellites: 'satellites.md' - T-Links: 't_links.md' + - Effectivity Satellites: 'eff_sats.md' - Worked example: - Getting Started: 'workedexample.md' - Project setup: 'setup.md' @@ -67,11 +68,6 @@ markdown_extensions: plugins: - search -# - markdownextradata: {} -# - pdf-export: -# combined: true - - extra_css: - 'stylesheets/extra.css' From 417bee39b98b401a02b9e166c98364a748bf862a Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 15:54:09 +0100 Subject: [PATCH 114/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf5eb77a9..4bdd53a38 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

-[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.5)](https://dbtvault.readthedocs.io/en/v0.5/?badge=v0.5)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=stable)](https://dbtvault.readthedocs.io/en/stable/?badge=stable)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) From f5a04c762b9b35e21385a10f9b5dc86a03045c0c Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 15:55:57 +0100 Subject: [PATCH 115/157] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4bdd53a38..0c7adac38 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@

-[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=stable)](https://dbtvault.readthedocs.io/en/stable/?badge=stable)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.5)](https://dbtvault.readthedocs.io/en/v0.5/?badge=v0.5)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) + [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) From 5cd1481a9250cf365713b778111d63264cc4d5e0 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 15:56:40 +0100 Subject: [PATCH 116/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0c7adac38..bc6d885d6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

-[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.5)](https://dbtvault.readthedocs.io/en/v0.5/?badge=v0.5)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) From 00eefcb67cb071d4e8e8b3eaa9cd3bf0ed2a63a8 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 16:53:14 +0100 Subject: [PATCH 117/157] Re-worked docs layout for future additions - Minor changes to documentation wording - Beta releases split into a new beta release page --- docs/changelog.md | 11 ++---- docs/changelog_beta.md | 36 +++++++++++++++++++ docs/macros.md | 4 +-- docs/{migrating.md => migrating_v0.4_v0.5.md} | 9 ++--- mkdocs.yml | 11 ++++-- 5 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 docs/changelog_beta.md rename docs/{migrating.md => migrating_v0.4_v0.5.md} (87%) diff --git a/docs/changelog.md b/docs/changelog.md index d58c50b35..11ff4c001 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,15 +1,10 @@ -# Changelog +# Changelog (Stable) All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [v0.6] - 2020-04-13 -[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.6)](https://dbtvault.readthedocs.io/en/v0.5/?badge=v0.6) - -### Added - -- The [eff_sat](macros.md#eff_sat) macro. +[View Beta Releases](changelog_beta.md) ## [v0.5] - 2020-02-24 [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.5)](https://dbtvault.readthedocs.io/en/v0.5/?badge=v0.5) @@ -17,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Metadata is now provided in the ```dbt_project.yml``` file. This means metadata can be managed in one place. -Read [Migrating from v0.4](migrating.md) for more information. +Read [Migrating from v0.4](migrating_v0.4_v0.5.md) for more information. ### Removed diff --git a/docs/changelog_beta.md b/docs/changelog_beta.md new file mode 100644 index 000000000..52b276502 --- /dev/null +++ b/docs/changelog_beta.md @@ -0,0 +1,36 @@ +# Changelog (Beta) +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +!!! warning + All releases listed here are beta releases and **must be used with care**. + Whilst we have thoroughly tested the code, we cannot guarantee an absence of issues. + + Thank you for testing our package, if you find any issues, please report them on our + [Github](https://github.com/Datavault-UK/dbtvault/issues) repo. + +## [v0.6-b2] - 2020-04-15 +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.6-b2)](https://dbtvault.readthedocs.io/en/v0.6-b2/?badge=v0.6-b2) + +### Added +- Added more documentation for effectivity satellites + +### Improved +- Improvements to effectivity satellites +- Link macro now has better integration with effectivity satellites + +### Fixed +- Fixed more macro headers (removed copyright etc.) + +## [v0.6-b1] - 2020-04-06 +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=v0.6-b1)](https://dbtvault.readthedocs.io/en/v0.6-b1/?badge=v0.6-b1) + +### Added + +- Effectivity satellites: The new [eff_sat](macros.md#eff_sat) macro. +- Limited documentation for effectivity satellites + +### Updated +- Updated required dbt version to 0.16.0 \ No newline at end of file diff --git a/docs/macros.md b/docs/macros.md index 470b73875..e4653b897 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -325,8 +325,8 @@ Generates sql to build a effectivity satellite table using the provided metadata ``` !!! note - As you can see, currently for the usage of the eff_sat macro we have the extra line of code - ```-- depends_on: {{ ref(var('link')) }}```. This is due to the structure of dependencies in dbt. Another method is + Currently, we have the extra line of code + ```-- depends_on: {{ ref(var('link')) }}```. This is due to the structure of dependencies in dbt. An alternative method is being investigated but this fix currently passes all the our tests. #### Example output diff --git a/docs/migrating.md b/docs/migrating_v0.4_v0.5.md similarity index 87% rename from docs/migrating.md rename to docs/migrating_v0.4_v0.5.md index 4200f3962..e05d434a4 100644 --- a/docs/migrating.md +++ b/docs/migrating_v0.4_v0.5.md @@ -1,7 +1,7 @@ # Migrating from v0.4 to v0.5 -With the release of v0.5, we've moved metadata into vars in the ```dbt_project.yml``` file. Your old metadata would -have looked something like this: +With the release of v0.5, we moved the metadata into variables held in in the ```dbt_project.yml``` file. +Your old metadata would have looked something like this: ```sql {{- config(materialized='incremental', schema='vlt', enabled=true, tags='hubs') -}} @@ -49,7 +49,4 @@ The new example ```hub_customer.sql``` would then look like: {{ dbtvault.hub(var('src_pk'), var('src_nk'), var('src_ldts'), var('src_source'), var('source')) }} -``` - -!!! note - Please ensure that your ```dbt_project.yml``` file is formatted properly and contains the correct hierarchy. \ No newline at end of file +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1b736d3e0..ff719ee2a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,8 @@ theme: logo: 'assets/images/logo.png' banner: 'assets/images/docs-banner.png' favicon: 'assets/images/favicon.ico' + features: + - tabs palette: primary: 'black' accent: 'indigo' @@ -25,7 +27,7 @@ nav: - Hubs: 'hubs.md' - Links: 'links.md' - Satellites: 'satellites.md' - - T-Links: 't_links.md' + - Transactional Links: 't_links.md' - Effectivity Satellites: 'eff_sats.md' - Worked example: - Getting Started: 'workedexample.md' @@ -34,10 +36,13 @@ nav: - Creating the stage layers: 'stagingdemo.md' - Loading the vault: 'loading.md' - Macros: 'macros.md' - - Migration from v0.4 to v0.5: 'migrating.md' + - Migration Guides: + - Migrating from v0.4 to v0.5: 'migrating_v0.4_v0.5.md' - Best Practices: 'bestpractices.md' - Roadmap: 'roadmap.md' - - Changelog: 'changelog.md' + - Changelog: + - Stable Releases: 'changelog.md' + - Beta Releases: 'changelog_beta.md' - Contributing: 'contributing.md' - Licence: 'LICENSE.md' From e2a9bc7fd68f8f8b7ef5dc009b78bd3d0cb8c010 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 17:10:37 +0100 Subject: [PATCH 118/157] Fixed README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bc6d885d6..bc7fd4e83 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@

-[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest)[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=stable)](https://dbtvault.readthedocs.io/en/latest/?badge=stable) +[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) [past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) From 546ab745668b88b7b91c949e9df59e54a02a0754 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 17:13:13 +0100 Subject: [PATCH 119/157] Updated README --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index bc7fd4e83..ad2560aca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -

-

News

-

+### News * We now have a slack channel, use the button below to join * Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. From 3e0d5fc3a574339dffb01d426d089e267037ddc8 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 17:20:32 +0100 Subject: [PATCH 120/157] Updated docs link to latest --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad2560aca..3fe9e0148 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

-[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=stable)](https://dbtvault.readthedocs.io/en/latest/?badge=stable) +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) [![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) From a50a3c6e774fa686769e6c143f1cd58b44f9cea7 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 17:37:50 +0100 Subject: [PATCH 121/157] Attempt at docs fix --- docs/macros.md | 5 ++--- mkdocs.yml | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/macros.md b/docs/macros.md index e4653b897..e03605992 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -316,7 +316,7 @@ Generates sql to build a effectivity satellite table using the provided metadata #### Usage -```mysql +``` jinja2 {{- config(...) -}} -- depends_on: {{ ref(var('link')) }} {{ dbtvault.eff_sat(var('src_pk'), var('src_dfk'), var('src_sfk'), var('src_ldts'), @@ -333,7 +333,7 @@ Generates sql to build a effectivity satellite table using the provided metadata Here are some example outputs for the incremental steps of effectivity satellite models. -```mysql tab='Single-part key' +```mysql tab='Single-part key' WITH c AS ( SELECT DISTINCT @@ -399,7 +399,6 @@ SELECT FROM y LEFT JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS z ON y.CUSTOMER_FK=z.CUSTOMER_FK WHERE (y.CURR_FLG='Y' AND y.END_DATETIME='9999-12-31') - ``` ```mysql tab='Multi-part key' diff --git a/mkdocs.yml b/mkdocs.yml index ff719ee2a..9ad885d58 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,8 +8,6 @@ theme: logo: 'assets/images/logo.png' banner: 'assets/images/docs-banner.png' favicon: 'assets/images/favicon.ico' - features: - - tabs palette: primary: 'black' accent: 'indigo' From 14486aa67d29a68b5591a801b79d6dd7251c6fa9 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 22:16:16 +0100 Subject: [PATCH 122/157] Attempt at doc fix --- docs/macros.md | 178 ++++++++++++++++++++++++------------------------- mkdocs.yml | 19 +++--- 2 files changed, 100 insertions(+), 97 deletions(-) diff --git a/docs/macros.md b/docs/macros.md index e03605992..abdcb516a 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -15,13 +15,13 @@ Generates sql to build a hub table using the provided metadata in the ```dbt_pro #### Parameters -| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | -| ------------- | --------------------------------------------------- | -------------------- | --------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | check_circle | -| src_nk | Source natural key column | String | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| source | Staging model reference or table name | String | List (YAML) | check_circle | +| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | +| ------------- | --------------------------------------------------- | -------------------- | --------------- | -------------------------------------------------------------------------------------- | +| src_pk | Source primary key column | String | String | :fontawesome-solid-check-circle: | | +| src_nk | Source natural key column | String | String | :fontawesome-solid-check-circle: | +| src_ldts | Source loaddate timestamp column | String | String | :fontawesome-solid-check-circle: | +| src_source | Name of the column containing the source ID | String | String | :fontawesome-solid-check-circle: | +| source | Staging model reference or table name | String | List (YAML) | :fontawesome-solid-check-circle: | #### Usage @@ -90,11 +90,11 @@ Generates sql to build a link table using the provided metadata in the ```dbt_pr | Parameter | Description | Type (Single-Source) | Type (Union) | Required? | | ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | check_circle | -| src_fk | Source foreign key column(s) | List (YAML) | List (YAML) | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| source | Staging model reference or table name | String | List (YAML) | check_circle | +| src_pk | Source primary key column | String | String | :fontawesome-solid-check-circle: | +| src_fk | Source foreign key column(s) | List (YAML) | List (YAML) | :fontawesome-solid-check-circle: | +| src_ldts | Source loaddate timestamp column | String | String | :fontawesome-solid-check-circle: | +| src_source | Name of the column containing the source ID | String | String | :fontawesome-solid-check-circle: | +| source | Staging model reference or table name | String | List (YAML) | :fontawesome-solid-check-circle: | #### Usage @@ -166,13 +166,13 @@ Generates sql to build a satellite table using the provided metadata in the ```d | Parameter | Description | Type | Required? | | ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_hashdiff | Source hashdiff column | String | check_circle | -| src_payload | Source payload column(s) | List (YAML) | check_circle | -| src_eff | Source effective from column | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| source | Staging model reference or table name | List (YAML) | check_circle | +| src_pk | Source primary key column | String | :fontawesome-solid-check-circle: | +| src_hashdiff | Source hashdiff column | String | :fontawesome-solid-check-circle: | +| src_payload | Source payload column(s) | List (YAML) | :fontawesome-solid-check-circle: | +| src_eff | Source effective from column | String | :fontawesome-solid-check-circle: | +| src_ldts | Source loaddate timestamp column | String | :fontawesome-solid-check-circle: | +| src_source | Name of the column containing the source ID | String | :fontawesome-solid-check-circle: | +| source | Staging model reference or table name | List (YAML) | :fontawesome-solid-check-circle: | #### Usage @@ -238,13 +238,13 @@ Generates sql to build a transactional link table using the provided metadata in | Parameter | Description | Type | Required? | | ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_fk | Source foreign key column(s) | List (YAML) | check_circle | -| src_payload | Source payload column(s) | List (YAML) | check_circle | -| src_eff | Source effective from column | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| source | Staging model reference or table name | List (YAML) | check_circle | +| src_pk | Source primary key column | String | :fontawesome-solid-check-circle: | +| src_fk | Source foreign key column(s) | List (YAML) | :fontawesome-solid-check-circle: | +| src_payload | Source payload column(s) | List (YAML) | :fontawesome-solid-check-circle: | +| src_eff | Source effective from column | String | :fontawesome-solid-check-circle: | +| src_ldts | Source loaddate timestamp column | String | :fontawesome-solid-check-circle: | +| src_source | Name of the column containing the source ID | String | :fontawesome-solid-check-circle: | +| source | Staging model reference or table name | List (YAML) | :fontawesome-solid-check-circle: | #### Usage @@ -302,16 +302,16 @@ Generates sql to build a effectivity satellite table using the provided metadata | Parameter | Description | Type (Single-part keys) | Type (Multi-part keys) | Required? | | -------------- | -------------------------------------------------------- | ----------------------- | ----------------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | check_circle | -| src_dfk | Source driving foreign key column | String | String/List (YAML) | check_circle | -| src_sfk | Source secondary foreign key column | String | String/List (YAML) | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_eff_from | Source effective from column | String | String | check_circle | -| src_start_date | The date which a link record is open/closed from | String | String | check_circle | -| src_end_date | The date which a link record is open/closed to | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| link | The link which this effectivity satellite is attached to | String | String | check_circle | -| source | Staging model reference or table name | String | String | check_circle | | | check_circle | +| src_pk | Source primary key column | String | String | :fontawesome-solid-check-circle: | +| src_dfk | Source driving foreign key column | String | String/List (YAML) | :fontawesome-solid-check-circle: | +| src_sfk | Source secondary foreign key column | String | String/List (YAML) | :fontawesome-solid-check-circle: | +| src_ldts | Source loaddate timestamp column | String | String | :fontawesome-solid-check-circle: | +| src_eff_from | Source effective from column | String | String | :fontawesome-solid-check-circle: | +| src_start_date | The date which a link record is open/closed from | String | String | :fontawesome-solid-check-circle: | +| src_end_date | The date which a link record is open/closed to | String | String | :fontawesome-solid-check-circle: | +| src_source | Name of the column containing the source ID | String | String | :fontawesome-solid-check-circle: | +| link | The link which this effectivity satellite is attached to | String | String | :fontawesome-solid-check-circle: | +| source | Staging model reference or table name | String | String | :fontawesome-solid-check-circle: | | | :fontawesome-solid-check-circle: | #### Usage @@ -514,9 +514,9 @@ CAST(SHA2_BINARY(IFNULL((UPPER(TRIM(CAST(column2 AS VARCHAR)))), '^^')) AS BINAR | Parameter | Description | Type | Required? | | ---------------- | ---------------------------------------------- | -------- | -------------------------------------------------------- | -| pairs | (column, alias) pair | Tuple | check_circle | -| pairs: columns | Single column string or list of columns | String | check_circle | -| pairs: alias | The alias for the column | String | check_circle | +| pairs | (column, alias) pair | Tuple | :fontawesome-solid-check-circle: | +| pairs: columns | Single column string or list of columns | String | :fontawesome-solid-check-circle: | +| pairs: alias | The alias for the column | String | :fontawesome-solid-check-circle: | | pairs: sort | Will alpha sort columns if true, default false. | Boolean | clear | @@ -648,7 +648,7 @@ FROM MYDATABASE.MYSCHEMA.MYTABLE | Parameter | Description | Type | Required? | | ------------- | ----------------------------------------- | ------ | -------------------------------------------------------- | -| source_table | A source reference | Source | check_circle | +| source_table | A source reference | Source | :fontawesome-solid-check-circle: | #### Usage @@ -684,7 +684,7 @@ CAST(prefix.column AS type) AS alias | Parameter | Description | Required? | | ---------------- | ----------------------------- | -------------------------------------------------------- | -| columns | Triples or strings | check_circle | +| columns | Triples or strings | :fontawesome-solid-check-circle: | | prefix | A string | clear | #### Usage @@ -757,8 +757,8 @@ CAST(SHA2_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(32)) AS alias | Parameter | Description | Type | Required? | | ---------------- | ----------------------------------------------- | ----------- | -------------------------------------------------------- | -| columns | Columns to hash on | String/List | check_circle | -| alias | The name to give the hashed column | String | check_circle | +| columns | Columns to hash on | String/List | :fontawesome-solid-check-circle: | +| alias | The name to give the hashed column | String | :fontawesome-solid-check-circle: | | sort | Will alpha sort columns if true, default false. | Boolean | clear | @@ -805,8 +805,8 @@ a.column1, a.column2, a.column3, a.column4 | Parameter | Description | Type | Required? | | ---------------- | ----------------------------- | ------ | -------------------------------------------------------- | -| columns | A list of column names | List | check_circle | -| prefix_str | The prefix for the columns | String | check_circle | +| columns | A list of column names | List | :fontawesome-solid-check-circle: | +| prefix_str | The prefix for the columns | String | :fontawesome-solid-check-circle: | #### Usage @@ -855,15 +855,15 @@ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, | Parameter | Description | Type (Single-Source) | Type (Union) | Required? | | ------------- | --------------------------------------------------- | -------------------- | --------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | check_circle | -| src_nk | Source natural key column | String | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | -| tgt_nk | Target natural key column | List/Reference | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | -| source | Staging model reference or table name | List | List | check_circle | +| src_pk | Source primary key column | String | String | :fontawesome-solid-check-circle: | +| src_nk | Source natural key column | String | String | :fontawesome-solid-check-circle: | +| src_ldts | Source loaddate timestamp column | String | String | :fontawesome-solid-check-circle: | +| src_source | Name of the column containing the source ID | String | String | :fontawesome-solid-check-circle: | +| tgt_pk | Target primary key column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | +| tgt_nk | Target natural key column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | +| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | +| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | :fontawesome-solid-check-circle: | +| source | Staging model reference or table name | List | List | :fontawesome-solid-check-circle: | #### Usage @@ -977,15 +977,15 @@ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, | Parameter | Description | Type (Single-Source) | Type (Union) | Required? | | ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | check_circle | -| src_fk | Source foreign key column(s) | List | List | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | -| tgt_fk | Target foreign key column | List/Reference | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | -| source | Staging model reference or table name | List | List | check_circle | +| src_pk | Source primary key column | String | String | :fontawesome-solid-check-circle: | +| src_fk | Source foreign key column(s) | List | List | :fontawesome-solid-check-circle: | +| src_ldts | Source loaddate timestamp column | String | String | :fontawesome-solid-check-circle: | +| src_source | Name of the column containing the source ID | String | String | :fontawesome-solid-check-circle: | +| tgt_pk | Target primary key column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | +| tgt_fk | Target foreign key column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | +| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | +| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | :fontawesome-solid-check-circle: | +| source | Staging model reference or table name | List | List | :fontawesome-solid-check-circle: | #### Usage @@ -1106,19 +1106,19 @@ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, | Parameter | Description | Type | Required? | | ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_hashdiff | Source hashdiff column | String | check_circle | -| src_payload | Source payload column(s) | List | check_circle | -| src_eff | Source effective from column | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | check_circle | -| tgt_hashdiff | Target hashdiff column | List/Reference | check_circle | -| tgt_payload | Target payload column | List/Reference | check_circle | -| tgt_eff | Target effective from column | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | -| source | Staging model reference or table name | List/Reference | check_circle | +| src_pk | Source primary key column | String | :fontawesome-solid-check-circle: | +| src_hashdiff | Source hashdiff column | String | :fontawesome-solid-check-circle: | +| src_payload | Source payload column(s) | List | :fontawesome-solid-check-circle: | +| src_eff | Source effective from column | String | :fontawesome-solid-check-circle: | +| src_ldts | Source loaddate timestamp column | String | :fontawesome-solid-check-circle: | +| src_source | Name of the column containing the source ID | String | :fontawesome-solid-check-circle: | +| tgt_pk | Target primary key column | List/Reference | :fontawesome-solid-check-circle: | +| tgt_hashdiff | Target hashdiff column | List/Reference | :fontawesome-solid-check-circle: | +| tgt_payload | Target payload column | List/Reference | :fontawesome-solid-check-circle: | +| tgt_eff | Target effective from column | List/Reference | :fontawesome-solid-check-circle: | +| tgt_ldts | Target loaddate timestamp column | List/Reference | :fontawesome-solid-check-circle: | +| tgt_source | Name of the column which will contain the source ID | List/Reference | :fontawesome-solid-check-circle: | +| source | Staging model reference or table name | List/Reference | :fontawesome-solid-check-circle: | #### Usage @@ -1208,19 +1208,19 @@ dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_sou | Parameter | Description | Type | Required? | | ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_fk | Source foreign key column(s) | List | check_circle | -| src_payload | Source payload column(s) | List | check_circle | -| src_eff | Source effective from column | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | check_circle | -| tgt_fk | Target hashdiff column | List/Reference | check_circle | -| tgt_payload | Target foreign key column(s) | List/Reference | check_circle | -| tgt_eff | Target effective from column | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | -| source | Staging model reference or table name | List/Reference | check_circle | +| src_pk | Source primary key column | String | :fontawesome-solid-check-circle: | +| src_fk | Source foreign key column(s) | List | :fontawesome-solid-check-circle: | +| src_payload | Source payload column(s) | List | :fontawesome-solid-check-circle: | +| src_eff | Source effective from column | String | :fontawesome-solid-check-circle: | +| src_ldts | Source loaddate timestamp column | String | :fontawesome-solid-check-circle: | +| src_source | Name of the column containing the source ID | String | :fontawesome-solid-check-circle: | +| tgt_pk | Target primary key column | List/Reference | :fontawesome-solid-check-circle: | +| tgt_fk | Target hashdiff column | List/Reference | :fontawesome-solid-check-circle: | +| tgt_payload | Target foreign key column(s) | List/Reference | :fontawesome-solid-check-circle: | +| tgt_eff | Target effective from column | List/Reference | :fontawesome-solid-check-circle: | +| tgt_ldts | Target loaddate timestamp column | List/Reference | :fontawesome-solid-check-circle: | +| tgt_source | Name of the column which will contain the source ID | List/Reference | :fontawesome-solid-check-circle: | +| source | Staging model reference or table name | List/Reference | :fontawesome-solid-check-circle: | #### Usage diff --git a/mkdocs.yml b/mkdocs.yml index 9ad885d58..e122d9b45 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,9 @@ site_name: dbtvault site_author: Datavault site_dir: 'site' +repo_name: 'Datavault-UK/dbtvault' +repo_url: 'https://github.com/Datavault-UK/dbtvault' + theme: name: 'material' custom_dir: 'theme' @@ -13,8 +16,6 @@ theme: accent: 'indigo' highlightjs: true -repo_name: 'Datavault-UK/dbtvault' -repo_url: 'https://github.com/Datavault-UK/dbtvault' nav: - Home: 'index.md' @@ -46,15 +47,15 @@ nav: extra: social: - - type: 'globe' + - icon: 'fontawesome/solid/globe' link: 'https://www.data-vault.co.uk' - - type: 'github' + - icon: 'fontawesome/brands/github' link: 'https://github.com/Datavault-UK/' - - type: 'twitter' + - icon: 'fontawesome/brands/twitter' link: 'https://twitter.com/datavault_uk' - - type: 'linkedin' + - icon: 'fontawesome/brands/linkedin' link: 'https://www.linkedin.com/company/business-thinking-limited' - - type: 'facebook' + - icon: 'fontawesome/brands/facebook' link: 'https://www.facebook.com/DataVaultUK/' version: 0.5 req_dbt_version: 0.15.x @@ -64,7 +65,9 @@ markdown_extensions: linenums: true - admonition - pymdownx.superfences - - pymdownx.emoji + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg - toc: permalink: true toc_depth: 1-3 From 9f59bbb933220242ec4ad223e06dc9e1cd553e7a Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 22:19:28 +0100 Subject: [PATCH 123/157] Updated docs requirements --- docs/requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 2f2e07978..6b4b00343 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1 @@ -mkdocs-material==4.4.2 -mkdocs-minify-plugin==0.2.1 -pygments -pymdown-extensions \ No newline at end of file +mkdocs-material==5.1.0 \ No newline at end of file From e124e3b6562eab569453b22411a29f75b42be70e Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 22:31:50 +0100 Subject: [PATCH 124/157] Set theme jekyll-theme-cayman --- docs/_config.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/_config.yml diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 000000000..c4192631f --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file From a73c535e72c80005086569f3b028e5917959313d Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 15 Apr 2020 22:34:33 +0100 Subject: [PATCH 125/157] Deleted github pages config --- docs/_config.yml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 docs/_config.yml diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index c4192631f..000000000 --- a/docs/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-cayman \ No newline at end of file From db88010f59336f2b73b672f14d01d803abea9e68 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 16 Apr 2020 08:46:53 +0100 Subject: [PATCH 126/157] Doc fix attempt --- docs/requirements.txt | 5 ++++- docs/roadmap.md | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 6b4b00343..a48f79e1a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,4 @@ -mkdocs-material==5.1.0 \ No newline at end of file +mkdocs-material==5.1.0 +pygments==2.6.1 +pymdown-extensions==7.0 +mkdocs-material-extensions==1.0b1 \ No newline at end of file diff --git a/docs/roadmap.md b/docs/roadmap.md index a77eafccd..2dd10d3c7 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -11,9 +11,10 @@ We will be releasing changes incrementally, so you can reap the benefits as soon ## Coming soon -These features are currently planned for the near-future. +These features are currently planned for the near-future, +and are available now in a beta release (v0.6b2) -- Effectivity satellites +- Effectivity satellites, [try it out now!](changelog_beta.md) ## Future releases @@ -27,8 +28,14 @@ In future releases, we hope to include the following: - Bridge tables - Reference Tables - Mart loading helpers +- Custom materialization for periodic loading similar to the +[dbt_utils offering for Redshift](https://github.com/fishtown-analytics/dbt-utils/blob/master/README.md#insert_by_period-source) - And more! +### Improvements + +- Staging re-work (move to YAML) + ### Additional features - Auditing From 11704ca04241f8091fcf8bb0e0784897422cce8e Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 16 Apr 2020 13:24:55 +0100 Subject: [PATCH 127/157] Doc fix attempt --- docs/eff_sats.md | 2 +- docs/macros.md | 174 +++++++++++++++++++++--------------------- docs/requirements.txt | 7 +- mkdocs.yml | 4 +- 4 files changed, 93 insertions(+), 94 deletions(-) diff --git a/docs/eff_sats.md b/docs/eff_sats.md index cdb0291ad..31fb864ed 100644 --- a/docs/eff_sats.md +++ b/docs/eff_sats.md @@ -65,7 +65,7 @@ driving foreign key. 3. ```ORDER_FK``` which is the other foreign key in the link. This is the foreign key that is going to be used as the secondary foreign key. 4. ```EFFECTIVE_FROM``` which is the date in the staging table that states when a record becomes effective. -5.```START_DATETIME``` which is the column in the effectivity satellite whose date defines when a link record begins its +5. ```START_DATETIME``` which is the column in the effectivity satellite whose date defines when a link record begins its activity. 6. ```END_DATETIME``` which is the column in the effectivity satellite whose date defines when a link record ends its activity and becomes inactive. Active link records will have a date equal to the max date ```9999-12-31```. diff --git a/docs/macros.md b/docs/macros.md index abdcb516a..f3aff01a6 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -17,11 +17,11 @@ Generates sql to build a hub table using the provided metadata in the ```dbt_pro | Parameter | Description | Type (Single-Source) | Type (Union) | Required? | | ------------- | --------------------------------------------------- | -------------------- | --------------- | -------------------------------------------------------------------------------------- | -| src_pk | Source primary key column | String | String | :fontawesome-solid-check-circle: | | -| src_nk | Source natural key column | String | String | :fontawesome-solid-check-circle: | -| src_ldts | Source loaddate timestamp column | String | String | :fontawesome-solid-check-circle: | -| src_source | Name of the column containing the source ID | String | String | :fontawesome-solid-check-circle: | -| source | Staging model reference or table name | String | List (YAML) | :fontawesome-solid-check-circle: | +| src_pk | Source primary key column | String | String | check_circle | | +| src_nk | Source natural key column | String | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| source | Staging model reference or table name | String | List (YAML) | check_circle | #### Usage @@ -90,11 +90,11 @@ Generates sql to build a link table using the provided metadata in the ```dbt_pr | Parameter | Description | Type (Single-Source) | Type (Union) | Required? | | ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | :fontawesome-solid-check-circle: | -| src_fk | Source foreign key column(s) | List (YAML) | List (YAML) | :fontawesome-solid-check-circle: | -| src_ldts | Source loaddate timestamp column | String | String | :fontawesome-solid-check-circle: | -| src_source | Name of the column containing the source ID | String | String | :fontawesome-solid-check-circle: | -| source | Staging model reference or table name | String | List (YAML) | :fontawesome-solid-check-circle: | +| src_pk | Source primary key column | String | String | check_circle | +| src_fk | Source foreign key column(s) | List (YAML) | List (YAML) | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| source | Staging model reference or table name | String | List (YAML) | check_circle | #### Usage @@ -166,13 +166,13 @@ Generates sql to build a satellite table using the provided metadata in the ```d | Parameter | Description | Type | Required? | | ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | :fontawesome-solid-check-circle: | -| src_hashdiff | Source hashdiff column | String | :fontawesome-solid-check-circle: | -| src_payload | Source payload column(s) | List (YAML) | :fontawesome-solid-check-circle: | -| src_eff | Source effective from column | String | :fontawesome-solid-check-circle: | -| src_ldts | Source loaddate timestamp column | String | :fontawesome-solid-check-circle: | -| src_source | Name of the column containing the source ID | String | :fontawesome-solid-check-circle: | -| source | Staging model reference or table name | List (YAML) | :fontawesome-solid-check-circle: | +| src_pk | Source primary key column | String | check_circle | +| src_hashdiff | Source hashdiff column | String | check_circle | +| src_payload | Source payload column(s) | List (YAML) | check_circle | +| src_eff | Source effective from column | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| source | Staging model reference or table name | List (YAML) | check_circle | #### Usage @@ -238,13 +238,13 @@ Generates sql to build a transactional link table using the provided metadata in | Parameter | Description | Type | Required? | | ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | :fontawesome-solid-check-circle: | -| src_fk | Source foreign key column(s) | List (YAML) | :fontawesome-solid-check-circle: | -| src_payload | Source payload column(s) | List (YAML) | :fontawesome-solid-check-circle: | -| src_eff | Source effective from column | String | :fontawesome-solid-check-circle: | -| src_ldts | Source loaddate timestamp column | String | :fontawesome-solid-check-circle: | -| src_source | Name of the column containing the source ID | String | :fontawesome-solid-check-circle: | -| source | Staging model reference or table name | List (YAML) | :fontawesome-solid-check-circle: | +| src_pk | Source primary key column | String | check_circle | +| src_fk | Source foreign key column(s) | List (YAML) | check_circle | +| src_payload | Source payload column(s) | List (YAML) | check_circle | +| src_eff | Source effective from column | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| source | Staging model reference or table name | List (YAML) | check_circle | #### Usage @@ -302,16 +302,16 @@ Generates sql to build a effectivity satellite table using the provided metadata | Parameter | Description | Type (Single-part keys) | Type (Multi-part keys) | Required? | | -------------- | -------------------------------------------------------- | ----------------------- | ----------------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | :fontawesome-solid-check-circle: | -| src_dfk | Source driving foreign key column | String | String/List (YAML) | :fontawesome-solid-check-circle: | -| src_sfk | Source secondary foreign key column | String | String/List (YAML) | :fontawesome-solid-check-circle: | -| src_ldts | Source loaddate timestamp column | String | String | :fontawesome-solid-check-circle: | -| src_eff_from | Source effective from column | String | String | :fontawesome-solid-check-circle: | -| src_start_date | The date which a link record is open/closed from | String | String | :fontawesome-solid-check-circle: | -| src_end_date | The date which a link record is open/closed to | String | String | :fontawesome-solid-check-circle: | -| src_source | Name of the column containing the source ID | String | String | :fontawesome-solid-check-circle: | -| link | The link which this effectivity satellite is attached to | String | String | :fontawesome-solid-check-circle: | -| source | Staging model reference or table name | String | String | :fontawesome-solid-check-circle: | | | :fontawesome-solid-check-circle: | +| src_pk | Source primary key column | String | String | check_circle | +| src_dfk | Source driving foreign key column | String | String/List (YAML) | check_circle | +| src_sfk | Source secondary foreign key column | String | String/List (YAML) | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_eff_from | Source effective from column | String | String | check_circle | +| src_start_date | The date which a link record is open/closed from | String | String | check_circle | +| src_end_date | The date which a link record is open/closed to | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| link | The link which this effectivity satellite is attached to | String | String | check_circle | +| source | Staging model reference or table name | String | String | check_circle | | | check_circle | #### Usage @@ -514,9 +514,9 @@ CAST(SHA2_BINARY(IFNULL((UPPER(TRIM(CAST(column2 AS VARCHAR)))), '^^')) AS BINAR | Parameter | Description | Type | Required? | | ---------------- | ---------------------------------------------- | -------- | -------------------------------------------------------- | -| pairs | (column, alias) pair | Tuple | :fontawesome-solid-check-circle: | -| pairs: columns | Single column string or list of columns | String | :fontawesome-solid-check-circle: | -| pairs: alias | The alias for the column | String | :fontawesome-solid-check-circle: | +| pairs | (column, alias) pair | Tuple | check_circle | +| pairs: columns | Single column string or list of columns | String | check_circle | +| pairs: alias | The alias for the column | String | check_circle | | pairs: sort | Will alpha sort columns if true, default false. | Boolean | clear | @@ -648,7 +648,7 @@ FROM MYDATABASE.MYSCHEMA.MYTABLE | Parameter | Description | Type | Required? | | ------------- | ----------------------------------------- | ------ | -------------------------------------------------------- | -| source_table | A source reference | Source | :fontawesome-solid-check-circle: | +| source_table | A source reference | Source | check_circle | #### Usage @@ -684,7 +684,7 @@ CAST(prefix.column AS type) AS alias | Parameter | Description | Required? | | ---------------- | ----------------------------- | -------------------------------------------------------- | -| columns | Triples or strings | :fontawesome-solid-check-circle: | +| columns | Triples or strings | check_circle | | prefix | A string | clear | #### Usage @@ -757,8 +757,8 @@ CAST(SHA2_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(32)) AS alias | Parameter | Description | Type | Required? | | ---------------- | ----------------------------------------------- | ----------- | -------------------------------------------------------- | -| columns | Columns to hash on | String/List | :fontawesome-solid-check-circle: | -| alias | The name to give the hashed column | String | :fontawesome-solid-check-circle: | +| columns | Columns to hash on | String/List | check_circle | +| alias | The name to give the hashed column | String | check_circle | | sort | Will alpha sort columns if true, default false. | Boolean | clear | @@ -805,8 +805,8 @@ a.column1, a.column2, a.column3, a.column4 | Parameter | Description | Type | Required? | | ---------------- | ----------------------------- | ------ | -------------------------------------------------------- | -| columns | A list of column names | List | :fontawesome-solid-check-circle: | -| prefix_str | The prefix for the columns | String | :fontawesome-solid-check-circle: | +| columns | A list of column names | List | check_circle | +| prefix_str | The prefix for the columns | String | check_circle | #### Usage @@ -855,15 +855,15 @@ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, | Parameter | Description | Type (Single-Source) | Type (Union) | Required? | | ------------- | --------------------------------------------------- | -------------------- | --------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | :fontawesome-solid-check-circle: | -| src_nk | Source natural key column | String | String | :fontawesome-solid-check-circle: | -| src_ldts | Source loaddate timestamp column | String | String | :fontawesome-solid-check-circle: | -| src_source | Name of the column containing the source ID | String | String | :fontawesome-solid-check-circle: | -| tgt_pk | Target primary key column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | -| tgt_nk | Target natural key column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | -| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | -| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | :fontawesome-solid-check-circle: | -| source | Staging model reference or table name | List | List | :fontawesome-solid-check-circle: | +| src_pk | Source primary key column | String | String | check_circle | +| src_nk | Source natural key column | String | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | +| tgt_nk | Target natural key column | List/Reference | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | +| source | Staging model reference or table name | List | List | check_circle | #### Usage @@ -977,15 +977,15 @@ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, | Parameter | Description | Type (Single-Source) | Type (Union) | Required? | | ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | :fontawesome-solid-check-circle: | -| src_fk | Source foreign key column(s) | List | List | :fontawesome-solid-check-circle: | -| src_ldts | Source loaddate timestamp column | String | String | :fontawesome-solid-check-circle: | -| src_source | Name of the column containing the source ID | String | String | :fontawesome-solid-check-circle: | -| tgt_pk | Target primary key column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | -| tgt_fk | Target foreign key column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | -| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | :fontawesome-solid-check-circle: | -| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | :fontawesome-solid-check-circle: | -| source | Staging model reference or table name | List | List | :fontawesome-solid-check-circle: | +| src_pk | Source primary key column | String | String | check_circle | +| src_fk | Source foreign key column(s) | List | List | check_circle | +| src_ldts | Source loaddate timestamp column | String | String | check_circle | +| src_source | Name of the column containing the source ID | String | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | +| tgt_fk | Target foreign key column | List/Reference | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | +| source | Staging model reference or table name | List | List | check_circle | #### Usage @@ -1106,19 +1106,19 @@ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, | Parameter | Description | Type | Required? | | ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | :fontawesome-solid-check-circle: | -| src_hashdiff | Source hashdiff column | String | :fontawesome-solid-check-circle: | -| src_payload | Source payload column(s) | List | :fontawesome-solid-check-circle: | -| src_eff | Source effective from column | String | :fontawesome-solid-check-circle: | -| src_ldts | Source loaddate timestamp column | String | :fontawesome-solid-check-circle: | -| src_source | Name of the column containing the source ID | String | :fontawesome-solid-check-circle: | -| tgt_pk | Target primary key column | List/Reference | :fontawesome-solid-check-circle: | -| tgt_hashdiff | Target hashdiff column | List/Reference | :fontawesome-solid-check-circle: | -| tgt_payload | Target payload column | List/Reference | :fontawesome-solid-check-circle: | -| tgt_eff | Target effective from column | List/Reference | :fontawesome-solid-check-circle: | -| tgt_ldts | Target loaddate timestamp column | List/Reference | :fontawesome-solid-check-circle: | -| tgt_source | Name of the column which will contain the source ID | List/Reference | :fontawesome-solid-check-circle: | -| source | Staging model reference or table name | List/Reference | :fontawesome-solid-check-circle: | +| src_pk | Source primary key column | String | check_circle | +| src_hashdiff | Source hashdiff column | String | check_circle | +| src_payload | Source payload column(s) | List | check_circle | +| src_eff | Source effective from column | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | check_circle | +| tgt_hashdiff | Target hashdiff column | List/Reference | check_circle | +| tgt_payload | Target payload column | List/Reference | check_circle | +| tgt_eff | Target effective from column | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | +| source | Staging model reference or table name | List/Reference | check_circle | #### Usage @@ -1208,19 +1208,19 @@ dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_sou | Parameter | Description | Type | Required? | | ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | :fontawesome-solid-check-circle: | -| src_fk | Source foreign key column(s) | List | :fontawesome-solid-check-circle: | -| src_payload | Source payload column(s) | List | :fontawesome-solid-check-circle: | -| src_eff | Source effective from column | String | :fontawesome-solid-check-circle: | -| src_ldts | Source loaddate timestamp column | String | :fontawesome-solid-check-circle: | -| src_source | Name of the column containing the source ID | String | :fontawesome-solid-check-circle: | -| tgt_pk | Target primary key column | List/Reference | :fontawesome-solid-check-circle: | -| tgt_fk | Target hashdiff column | List/Reference | :fontawesome-solid-check-circle: | -| tgt_payload | Target foreign key column(s) | List/Reference | :fontawesome-solid-check-circle: | -| tgt_eff | Target effective from column | List/Reference | :fontawesome-solid-check-circle: | -| tgt_ldts | Target loaddate timestamp column | List/Reference | :fontawesome-solid-check-circle: | -| tgt_source | Name of the column which will contain the source ID | List/Reference | :fontawesome-solid-check-circle: | -| source | Staging model reference or table name | List/Reference | :fontawesome-solid-check-circle: | +| src_pk | Source primary key column | String | check_circle | +| src_fk | Source foreign key column(s) | List | check_circle | +| src_payload | Source payload column(s) | List | check_circle | +| src_eff | Source effective from column | String | check_circle | +| src_ldts | Source loaddate timestamp column | String | check_circle | +| src_source | Name of the column containing the source ID | String | check_circle | +| tgt_pk | Target primary key column | List/Reference | check_circle | +| tgt_fk | Target hashdiff column | List/Reference | check_circle | +| tgt_payload | Target foreign key column(s) | List/Reference | check_circle | +| tgt_eff | Target effective from column | List/Reference | check_circle | +| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | +| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | +| source | Staging model reference or table name | List/Reference | check_circle | #### Usage diff --git a/docs/requirements.txt b/docs/requirements.txt index a48f79e1a..bca70311e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ -mkdocs-material==5.1.0 +mkdocs==1.1 +mkdocs-material==4.4.2 +mkdocs-minify-plugin==0.2.3 pygments==2.6.1 -pymdown-extensions==7.0 -mkdocs-material-extensions==1.0b1 \ No newline at end of file +pymdown-extensions==6.3 \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index e122d9b45..1a1f9782d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -65,9 +65,7 @@ markdown_extensions: linenums: true - admonition - pymdownx.superfences - - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg + - pymdownx.emoji - toc: permalink: true toc_depth: 1-3 From 0dc138c3c2c98e185494dc481647fb984ff9e413 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Sun, 10 May 2020 14:37:13 +0100 Subject: [PATCH 128/157] Removed docs, moved to a separate repository. https://github.com/Datavault-UK/dbtvault-docs --- .readthedocs.yml | 12 - docs/LICENSE.md | 177 ---- docs/assets/images/database.png | Bin 9717 -> 0 bytes docs/assets/images/docs-banner.png | Bin 13950 -> 0 bytes docs/assets/images/favicon.ico | Bin 1150 -> 0 bytes docs/assets/images/logo.png | Bin 6512 -> 0 bytes docs/assets/images/staging.png | Bin 135292 -> 0 bytes docs/assets/images/tpch.png | Bin 286528 -> 0 bytes docs/assets/images/warehouse.png | Bin 20919 -> 0 bytes docs/bestpractices.md | 138 --- docs/changelog.md | 207 ----- docs/changelog_beta.md | 36 - docs/contributing.md | 37 - docs/eff_sats.md | 156 ---- docs/hubs.md | 162 ---- docs/index.md | 90 -- docs/links.md | 152 ---- docs/loading.md | 215 ----- docs/macros.md | 1277 ---------------------------- docs/metadata.md | 161 ---- docs/migrating_v0.4_v0.5.md | 52 -- docs/requirements.txt | 5 - docs/roadmap.md | 42 - docs/satellites.md | 128 --- docs/setup.md | 188 ---- docs/sourceprofile.md | 97 --- docs/staging.md | 223 ----- docs/stagingdemo.md | 169 ---- docs/stylesheets/extra.css | 37 - docs/t_links.md | 134 --- docs/walkthrough.md | 76 -- docs/workedexample.md | 59 -- mkdocs.yml | 79 -- theme/main.html | 36 - 34 files changed, 4145 deletions(-) delete mode 100644 .readthedocs.yml delete mode 100644 docs/LICENSE.md delete mode 100755 docs/assets/images/database.png delete mode 100755 docs/assets/images/docs-banner.png delete mode 100755 docs/assets/images/favicon.ico delete mode 100755 docs/assets/images/logo.png delete mode 100755 docs/assets/images/staging.png delete mode 100755 docs/assets/images/tpch.png delete mode 100755 docs/assets/images/warehouse.png delete mode 100644 docs/bestpractices.md delete mode 100644 docs/changelog.md delete mode 100644 docs/changelog_beta.md delete mode 100644 docs/contributing.md delete mode 100644 docs/eff_sats.md delete mode 100644 docs/hubs.md delete mode 100644 docs/index.md delete mode 100644 docs/links.md delete mode 100644 docs/loading.md delete mode 100644 docs/macros.md delete mode 100644 docs/metadata.md delete mode 100644 docs/migrating_v0.4_v0.5.md delete mode 100644 docs/requirements.txt delete mode 100644 docs/roadmap.md delete mode 100644 docs/satellites.md delete mode 100644 docs/setup.md delete mode 100644 docs/sourceprofile.md delete mode 100644 docs/staging.md delete mode 100644 docs/stagingdemo.md delete mode 100644 docs/stylesheets/extra.css delete mode 100644 docs/t_links.md delete mode 100644 docs/walkthrough.md delete mode 100644 docs/workedexample.md delete mode 100644 mkdocs.yml delete mode 100644 theme/main.html diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 886638aa5..000000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 - -# Build documentation with MkDocs -mkdocs: - configuration: mkdocs.yml - -formats: all - -python: - version: 3.7 - install: - - requirements: docs/requirements.txt \ No newline at end of file diff --git a/docs/LICENSE.md b/docs/LICENSE.md deleted file mode 100644 index 4947287f7..000000000 --- a/docs/LICENSE.md +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/docs/assets/images/database.png b/docs/assets/images/database.png deleted file mode 100755 index a3e279b176732d58ffac40b15381f32d99609da1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9717 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 V(ZVXC4@-!GCI2Uh73Nof=r>8fg{lAm diff --git a/docs/assets/images/logo.png b/docs/assets/images/logo.png deleted file mode 100755 index 56214b42a224febb69a589cb954036ec1169b468..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6512 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 Bg46&2 diff --git a/docs/assets/images/tpch.png b/docs/assets/images/tpch.png deleted file mode 100755 index 7845e62faa75d981898b744acdc9a7528757fd7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 286528 zcmeFYV|1p=(l8uNY}>YNClgO>+cqY4GO?XZY}=XGwr%sxoM)eX_B#97Kfm?;dG2-H zD_6R^y3}3ORn>Q>oQxO@Bqk&P004}HxUd2M0LVB1z!!6H(9afB2SyA40B965At5;l zAt8J@dmCdjOCtaP@z8`MFvYlEsDlSo6afLKA-?gGY547u7dS_{M8trC5Crgwr$f2l zEd)eBL_}2hEiC{I%NG+wy0mrV&E##87zSDPQkeMp&fi~;28 zOdvp(ej);JyaMStX2UTtP^3s80)uS&0@3s9SKoYDmzUSu#2aZ%o5V`ANc^Gsar&SW z&KR6F0SXZMJ$?9>$pJqvG(g2KRB$K&d`E%S?o%MYfDJ)q6hS`#_+N-JN!5t~-;6A2 z?m{E)15d#KQik;*W&q`4c1=)gX7q5s02*5G1`h_e!PJ0p`S<%#2XRx7k)hQPqSTlj zySPn%5Sbji%Aittr5hQBNkszWB%te$a9(J=D+`{$Av1l`QXBw$hM z{lwuIkc|upCnms|b$0l>9^HjbeS;0pvhLWj(l{xzOpIP3=3NFz}W z6Y6aWTwB2Ql)MPiAy4ckFL2fKBbi}*6Erw-UvqpS3VmmpS(~5UuxWa-R|KOPf-&Q8 zNl@Dp#PG|I){`1TPTo6QIHM=gfM-CiX{fQ#kj+d0;1naC6c3q?rDifp8p&KX>{2N{ z5vw?AIEnoYQtv>WU6c1aBMf@*NHo@5MEBet5)Y;Z1B2D$RQwEf`iS}Cb09M z;O-){vW6z6b4JubMeLJ=L)lv*Pz2x*;~maLIVaXK!LFCqGY#S`ik!5|5LyzG`=CIh{1h;@(n8F$kIpFB))sHsfRaou`|ehNZ33|g~7X*D37<~{)bkM?XHPQ zJf7;kMS@cxXUx_BoL+SRB(BhUeVj!*i0H}yvgri+d5t5a17va50GkTkikPHK`L5d_8$(*A%Gfz4bd-0SrTTRH*jb;i*Y9^G^OeYni{pA zBVr2C0$DD2GRb4gz4lOD8@}cVYMMO43a)nHDeiKY45$B z)n#KxP-r#aDUnRy5j#?2zzGw0bnlWYZU?+hsLBqEE0Z^BXVAm8|Dy^}WVT2ZeiPz% zh|mDX9!o)roF7%;wgI-V=RbLSxCmp1@KfP4HrJ&HjS&wJR1h2CACP1bT0^5lg@_R) z{0#-{$wYpr<^w5Em?4?L9T67?&x$?sCz74~u=)kKYsi?TDOEvyL7IjC5R(-fycfS4 zv8%a9xW{hqu%|q77H<*PO1VJBCM92(EVoKg9WOPED2ZZ(w}-vQyBFEk*4EJ${;KGf z`Ktd!`2>88w&$_Or=aw6Us31Su_UA=OkI{oq+Q5MC~qplOuNod9ncD;Jk~krKIUG) zuR^x~zN|}LHD4}ovTQQ%QbAs3N_I|5Y~C;J&RI) zPMAfNA&)adv80YP)*dGIl*?{f@al=m5Pj2vZG4DFzq@8%7qXsqAHDS4Ne7 zPX`1K(t4?^;)B9LNmB8JN@vcygn#i?Wo$vUa-PVJA8CbI1&5MDgK_yj$CzJe3DO#A z4OyZzH~UouYKc(=N;!+HMX+-4d`v@>b9Mv2N8CNe86ZwjRFX{LxO)Z?=Ua=T7Mxb* zx%*mNix2%=^7^QyG0F|KgPiPFI&sNWtwFi zIWgs?)T$*eq1Mtm%&vBG_qGkZmV7baGVQKi)Su!BrT=Aw=%cWfuaT(u)n-I+gmgrk zDo80=sah#~Zsb=`TpU?thNs@OiUrt-Pn53g=OW+)G5#@ z>Qeo3drf22!ScfLhI2Mo02j4`smtT`;V!x3>cQfL;bHuC-O^EFZHYttjpRW%EeIN3 zk_hdfCT|O83u+Ut9?kD&N6PN6&K_78|Sq5y6*X|mN)Po$8I&; zC){{$7_N40&Tc$6tT*Gg>pO4LgUe!v4t)phm8BOz8{fQ>b+UC*bc#B|eA0dDKY9V) zd~baVeY^RCyWISWzJm+o2w3=Af3X0o^~drD6G|6E4M>M62pR}b4(5eqfB*;)=VwtVDi|%QVXk9t%WUsS3p#7F7h-MsX0~^D()^$`v$huaT96J#Q4GypJ>q%!7_fDzx!a^t0JsO&W_?ksr!j%_a^ zIWFG;$rtX!szT->W8K&#C`So@(7bOogoPXg>9R-k#B{9zrMM+uBxFeWEHSHbD6jhM zQP27HRLCLz;PSw6lqIWG|GYNWH&t^w0`QT(shq2Se@8SCyRBT`|)Cw8~HKp2Y zBfG6#FXHfzGLlX-4Kx+xPOa3cx^m%GL%Ux)SwKGKluG0%iRMcSn9xbdZ?lf2Aax~=?uUxer zTlVip?~G11tdr}v6biid<|E@Hb0fD)s!HaigVN?LpEWb&z48W%j6aOa$Ea94H+q() zznv(x+LYeUUzcY!Ts5q%<2JQ7k2h7=gV~8~MHO-FxM)B2KP3%lrM3>ccoiQkgk$Am zjk9vHu%6E^x3@r8A+p?Eq;wQnRBo)#dnLX!T$r5F@d3bwz`kBt3|F$j1>suq?RuJS z=;igMMNdY@vk;9rq=9KLs`r&kl&^^iMKv@w*W)~m9XCE3xDT2~mE!1leYelu?Hl@) z8z)o*QUoefl;yq3Tl%)1(#NzgA;YucMti&3e*<_hp?TS1d!75zZ;+%(htq!Q*}NCF zldumP97WX0_3E8JQ(gO1GBY<%n^e(oPO(Pj&g~(ibJObl?tXbk#YSlDxDnCn_A-B~ zvAvSMGI3$J;pWtMd4Ad6{{ZnYf=kZx)AQ$D)wz!G$1g)9F-d1PZ`!Tu{_NfPn zC(E__9#$mVoz2-s1Dps9k7v@2^`qAkSR@<_UIw4`WBldO(&gzy{)CdwXh)qF%$vIp zv5BE@7h+Am7$CGVGYyRI!X3cmkIf~(^&N7^nwO+H{2H#j_%6Qc(63w3KcA@VpTbZBwAWv!GpQTSG&aiLtSC zTseze0O`YVYcIrs;1iE4qa=<_VFUV?LQib_lIAbVq{CaEaRP*`xSGRfxIpsz3n-yL zdZmF$#c5z;MXP6Mqi;m(YGwO58UTRXmGkq}%E(a<-_^>}+JV!Rhwxt` zI6vQiH`5W~|7(b&1rMRBv>d*WjlB^*3oSD(Js~e7K0ZFTy`eFug0Scx;Gh5T5SltV z+H%s-xwyE{x-ik&*qhKXaBy(Y(KFI9GSYmGpmA`scGPpFv34N(pGp2bkFb%0fxVfn zqnV90{_lD9^lh9Rc?b!ABl`36KXe+on*9fpwZk85eKJV*yN8Z}mY(jnbY1b$Nr4muA+>&{ANbYh*C6X{BdiqlXvk4;h~3rMC!${5%&f8rw0hclzk_hABgxL9@NkNT3$Lo5%Uj^Dtz|VC=g`(KcN@*4Pxp5{wL(bx&oj5P~^A&%p&IaBkVsRkMaEih&fK3 zJsJLw?0+)P^$(nX67_$~|Np3Y{NA2uZ9t&BL+&TamP&vHO)sZasS3?sePfN~{89KG zTeI@m?4FW+8IfFgltK~$&Z|fH7F~YhahI=`WbAKg-lLg3cx>y>ZcQDk*qaRNjFGU8 zJujkkx_P{_+;f~1iYJCuNR?raDi&9jngYh_W2*8oOxjcW1D&?_$m8Dh-yb(}eP@SW z#R-&mfyp!`HiuajR0lcXe(Kuzd_IAi@k8e3N+Z-X1nZUjT zmfEhA=&x>*^E(gFM{!@XhYdeun?|T8m&eU7w8mmccksOeF6^S|uY+YAzW2`M(uhF1 z?uCKJJ^W%n(L*;$&FZcGaK*5ByGPuo?m(cj)(+(2b0LGY2eUO@0bJFthZDK-ofJW^ zdYdgh#A#MHp>8zbLC5*~He!nQ?V*D7BPghEP@tf4qTDX8y0W9R@{ZjVZ&jcibWo)9 zlDALAT^Ex4mcRvBq|m>@_pb2Vt|UK=*Xm6f2q2_|+fT6&YRg5rGC-#~iu@{>U|{(~ zy}=-sFkSiB@1crI&}ih!o)ET7iZJ+meB&0+Xp&DYQ}i0el`yT4w0R2IF*q*~=q2`y zEUVPKO>Z~1>yV^Gepk+0P~Ra*;lxpPGNDwWINv}iw53?C<(;KK`|E<1N@aLeZz;p? zb|-!%rZ;9<6+woK+~X$>R?k(?-*&QYA;PADG=pSAM@^Oc2rV}IEVNC+2zsAxaCe-r!|>no&)|lAK`PE;o+E3+Q#cM4rN=)|V&9vNbid0y!4d<~w-MT<*vf zE(c}upI9(fIG1ptoQF34qP&ljeubF?jxy{One~JpUtT!n=*oorERLuR@ip3lUnWm&W=0S_ z4+SkY;GenFbH0G7SGk<1k9MA)Pb{P+Wl;+7%I_Yx? z?`);UGho>a<=V5?<*no`h7o|HnBOitdT2QyC#&Hr5I%?|&OzI3gdseBa{mfmAB09R z8wIoFD)cv>KtBUMX^ScrX)?}OuA;#yOKk$oSW^>|km299sHHm~M};TV|pn=)e3M!aLCQ^0~BERjbbl+2ju2XAF)G9L7{ zs74K&zciU-k(u_%@0h9F>$6L<7FqPYBz>#GeDPj1XU27J*uL7=rXi)p_JJN@{c=Kf zs0La2kxXxj5)i3`u|%%kD2?8=yg6^MY&+Tk8ZOn1=DGOVW1@RxZ$O~w;sts|9go9% zgY9fC+G&xYN+2>kOf}H4kE=L*Hby+M*cf-IA3=jG^mi8|#rWxa5Q_npvi;<5o$tdw z{Br3w$c#_Zd+~tG_VvYX;5j|DT~}J^?REX>!=)xHE+fE7)C$)R2K}^Fs*Dx@A6KwI zpq{R~AI`H+^)`5}{X)KdG4XySt_|1xiCo=>Y7pn_g%8b}@UUEcY`8iQ`|TAQXUoxW0j| z9`HqOpJK_Yy^vSnaR=soT)|enPX_miod$XD@ziM#3;zmhqyI4lW%}NTx4AU1VB1jy z)}+T_{n2OddCUassS1_gMuYuT^lCcVCzOgXrHkbLyN>%Hnmh;}jbgutReWm~zA!DUa6t%Q;O(WYqT|We z!j>Ustj3Lc6gW!?3K^#OT@s<1l-+9j*;G zWqOY2(}KpGoEjYRakhy>$@mFNOH-@fdRT)cn!s4@S`}+CixZJFBN#Gzwq0&FSHAC1 zboC}iO&;~nQNo?q=dY>B-Og;JQ}gHqt3xPuA%4zJu=;zi4nM8lOe_&<_v@$*8t#sP z6u&lu^|xxh;9OaUbBa2%qS9&ywstzxNkSGBTc`{AT?G0ri)JA(k*BqxVJ`6yy7odsM2yV~^VFX%XGdAYB&!Pg7ORI$WKVXC>r) zg?5YdN9q5z91uv!W@#-X3femLW-}fp^@}w;t}svLC?V2Iz+gZDucGeD@sN#XM7Ld{ z7dh-&kzvXB#M@u^j!6#+rV9^JJCn5v+IGs>0+h0p+(pjctil#)C1r)~;A|ZEa_f|m zFf#$I`)58^YhSyf86L#&ZYkYDFPGN&rSa`JWC0N}sbX7G=dy*7bwQZy`kU8~31!mP ztKo^AF{7c%1qOw4HcTegq>g3MbqY-zX+B*M=#<_hq0u0V>)#ZJZvRu zM)pqSDWxvHx6YGoJCW;8l_7(L)#jQ-cz%z5d3i?Bn8THalPjU7?D1~)zlIhmWm8Gn zzH&aQiHZox*p?zWH7%ROXp;O2W0l=jO@+KXx-cYd5jQ@pY;gRhU7%@lp}1OcNdS!E zzN~yU#pzY;lcqog>pyuN*IBw8G06Lf*|VjHvpv5jH{(p6Ss{!WU+vmP^j`;mzZ4mM zOT}>uYM}jYUd=hbI4F-kia-J2WXnGA0#rdsn^NeaZ_DuHzMkI;m~!IC#E`2I zJlWT3GpLa#!5UgZw03z{@+3#KX9Pc9jS;8E1k@ZUdtE$x}5&dE%+h*N55(kWMNCWF#UXyh~3bHN7_}+z? zD07B(>2}G|i{3j7G}|WbHZbKqFALy_8XOs(v&6NHZj4s4@37C8w*or&G_gpYFVY@u zW0}C!=rS#+`k8m;k`z+c8L-u6K4%pkwi6qtjFwfEG)^E2eI~0S2%ep9=(rsxlx3@) za*|__15p>C_w-nvA2ik33$6*sVk_ih=(HFv9t(86uTM}_rrki)a(q4Q&Mvd4OrnWT zvmB3APR7;Y#ON9=Plm5`^>#?t6yu-ecU$If@bSo z?02Fd6(u+U-!Cidgzz+>2hEm3dy8gSaz`RP)+Dxi{f_?bfvSXj(>51#3Id(tbGD|G z9-n5??C48y@Sz-f=h0|LV~Wo>QEw@qB8-QIdVnZAWj6x5=8u1sWnT{m%f%-J=L(H2 zI+GJmciCZEK!m%vZE}Nx_T9@IOh|~L;kiGHiL5>ra42cyV4;+@e*_TTB7u%igiiov+%jmST%Zrdq@i5B9tRm9`P9z$pBx zy5LFG`G&Ggs}_iZb5IRoscrrpSB{!>D(nSQ)Wsxvj`f2u5Sp5NsBxb(vxSn!)$whR zK3C0~$*-305tQ{JNxP|O559RXVKccCIJn~qpkYC^sM_NU(XjqbPpo=z@T}*=RoOfX zssf@TAh04>ZojNoAU2@2gn8Jk5!TZ)7IfO(r$wjDQR;^``kQ$#LjJltm1V_k`y7cU zB=AKtd52^3Rue&TMUa@s&(64>-ZCNPP{xBHMh@GX?Ir!MtaR&~MFohj^F3HzZess! z+KJ)m0HaE`Nl1b6V&m)#ZdzUPDtDhbq=VBNjeN2^8 zQ+rn3@L(|=^~p?jih&zBQH2BWxF1Ljkpeisif4)B1>bLZeZa?%!Ohc znO4JlSm|lR6Jgh0E9;I#p&(q5&F<7JS?Z1Qpm;`b5WADX8lrOsp&0w;$FH7RR3@B% zAMa)u#=_WQZqW8A-NJzgw#%lmB^O;3b1v8Ua6?X^E_Bdl82cVi)MpC^r`;uI3~OHl z6)@F^;d3<^?&>+={HP_zT!~7x{NIrlwgH zBFYWGwx_UJdv)iX=clcdH1*sLwhcIR93m8}h})f?d1XBwlN(9_jkMZ52^$%MCociG zVb=W%eOqjQOPlyg18J6+A*tdwsRE%eHyUQ=*I~mx(#T%dN^%l?IEO^8-j$xuu>k>F z+-CNDn`Y;J`g)B0sOr3X$>VV)9kgeI5{ZM3d7anUsq_4}l_)bh8`$XRn792r=0_G~ z57*IJAP&cZS)*}?YqlxQo3)Fgp7jfbhuxxr~hXE+fmjFDQVe~UWxf4M7& zMaguoo~53*ZxJq$t|u1O_P^+;pet6)!u2Xg^O#4$r#hBO8)EL^%`^3Oq`HTKE22+* zGu;4PGo|;u2REkkfWaXGlfw_?<7Ow1h)Nk6mm%-9ou{xTNs_e>WTt(q71y8+Zr8wm zQmxfd=WOakEgJp`UkR%Gk@!Sea(fnB@k-yF`C;NPt)%>^q~N)F;`^kA)llE4u=9PF+p4aV6_6`W3$g;ZCHfm5 zvg|b1N+dc#?F?7i+3lTA)7^Deqw78VXZEd|&ehO9Ymu;W5h?T)@@M@HBk4^%GO&#jkLhZGbbame58l*$thP!p%>Cwf;zf)Bg zniv?KXmZjC5y{yo;h|jv_BYAw)`G*6zHDbDMzM3aAF^IEC3TMLUGG$*=}BIXJ^c2C zXgUw<@4J%aGn6Z9M)#Da>gWSCtnI@tzD!8Sz-h)?GKG^FVbT@GKI42^Jg_^Fnd?#v zy1-rZ=K;*?!?j#d{r-A6ey)DGsr_|}YxYBmr`QHy!ZUA{UIUtxsPBUJFjhYlu#>v( zp7;ROQ(9^tLP!5-!33}S*SbIi?8EjU5v^qfhVT~Qm@YglMQadOwEo-k<+&*fSEIqp zc*Y_HZ|lQ6i17@^!Wv@c8Pg+aW+H2-&5{p%(O|>aFE!BBJRXy5nN=vArJ*`X6LS7O zqKuTQU(M$ox)H=z5FH6-qYN3DK2+-uqv*WeO17<@c~U3r=q zHV4zat8Yw5L)@6R0`eGaH{Pe9G%{its1xLu}`iURnEUPc&CZsfYOYgTkgQBc#WbZyPQ4fYI6 zUdbI~82)binZlCHN)2>}aYDbA-EdL;@BHmLvGELMz`oRJo3d0P zb=gzWqgB?-s!U~AYuQ7)r0!lgu0Q=XB=`Nqyd*4ULTTwIi1w3QeC%hcZHqYpt3Xl% z+wG}jE$DWn1S_EefUbMsKwk2WmE>630@%F6OFArB-YlfFJ^Hw@wuXf~qjPOm8(Y%L zV{=UI_$v!-x8pDmP7dN-xXIK^P@cln#$(9+;p9r*!Y!=D#jacctli1ocB!)nO`In0 zoYo4ijaE=t|Nl};$N+Fe!$AOPn~N32*vXzPccqEoA>xS?C5qs+b)o9W!4<)Vg_GN%s3M4^`db-f`hLnxag8ITh3&dDc1E$$8I|eI; z&A+DfQ_uP8+2+%kdO9_w`#s9I1k#s0C1jFv@B}Mhh`q4n?9=?+iV1I0&e4E|w~@y1 zA52*_d4C&QOqQ`pr;^xkAR>-KgMV2mGBFP#7)qY@imFtXI#?EoGY*9^4 z=Q2Fst^0s+Yq;*))3!-~*bAIXla6K+wTO557EYw<*?Tt>`BYPKp$UA$yr^wSTOoDG!fZe$HBqE1$mdReixTOLKBXI+t)QoZv1(L@m@wI zQ!4Pk6T`-+pGKRsr6ai2?$D29vBKJJ&oNJbjgmT7H>;&s)mv8?{4;YOL8|zz8NG_at{cq*QK=Vq$%ljSw%^I1EB*7cfePto1Aem zhxPg}(OQTyEaSf!p8rYBGXmDmiF%8=K!T5USchgF-)C$H-*0S$%c9W^q>mzKdOlDu zYrC@QG6SzJ`c$%O`=b3#i~mz+>&l1UR2Y-!;@@Q(Y3T3?K>@TM#=TgKnZ5P!B105PXuDg9Hm5+4xivtNxr=bzXHfS9x$aQO$G zK1+<$pZRHHz(DOk@%`-%Rvs@&l} zDF2g|Pv&v{kNN*~fBm24|36&zF#;g}*$MP0;F`_$U$nHez6l@?|E2hCCaZgYx}@6X zYPX#9nHT5pT{2t%xcSo-fWy1)M6y-c?@3Pobq$LH`SE?+@C}9{k;SB>qy)?wTO+Da zY^OH&oBaCI{1ko;dcTT(oZ)+$DaMC1`p*P@W2|Cbvngc% zHhlbwKrk>cO=k@&xx5F!e_1+yiy-fB8y`46Zwo#+T<}=~BECVjP4o=#e?n413?Xeg zI58IhW*$dM8-?hs<!~q;F4g@MMg9KoQEs$|yO{AmmsJ>`#Q%Ui68V(FbZH0qUru~dCcia(uxzyShv4z0KUa-fsv>g#Uqp%_if<5< z;4-=m>x?@Xi?-fZ)?7>2tL??IKhX*M)rMS9Uch?S;g`v!B~#Sv?ioL4(p!>U+{uY* zMixonWV%1Q_b&K4c6y*1s#;0TXR*(T{LzMObHM1!(3Hg~khbSOhoepuuIvb4Q^ZJ8 zWq(VhO+W$omM`E!u?g+~TT}+{D_hR7*o$2JRAioPdq}t-KNUjd`=qC4qbo&NA{xO$ zNl4*sxi#1Xc6U412i>b`)EghS?>2U++%1vcE`qp>1|I(n(??L_ob%6y+S9blI}hdF z77Qr`4LqE#;-u6|g_8-p{V@l0WboQrG<~}%$W^xk&iJw+HbESemIB(J8UfoOxvhfF zVv!UliBlj>BDq%)Kg8By(tCP!oPULK7}%|v!Q2) z27W~|=r6kHYoRL>@NTHcC)x`=V)D(4KJALL;!*W}@W%d3-Z0%d`V3*1Q2f-JYKZRK z?Z=zVuN)V+O4ToxMIhmm#`_sv(7?M$^DIdh*74(*=60P&@WLqV2YS0Lq|-!up}DB0 zM;sKE3Ye4hC@Xmp!R^G}sYZYd4ea48l!Dl}o)CGJhiO=-Om(cyNk&uGNQ_I`7^blv<4H6=BF&YChY|NGOFcgHn+X|aeyBr>|IgWDEJa&WO z9q##@)i31cZO8S2_f0j7L3|c?_nj4|mQA8chV;@CwBMRfE9=vKZbX&H>5Nn=a@F5M zlO$(O(V1iZIG@=S4}w9Rb=*z zK?U)I*?ZoAE!Rrz%-Fjd=!jBhjFVv=WLOU#)CDBB8*~bZ-;F4+1 z9oHs!81Nk6*CYBep{`?=gH(Y=Qw_`{EcKalrl`yHG#XbY z-Vk+qfwdk`!R4}kd+nk6S;oY}`N*t|L;qCVn357gC&pq6r8#omFH!0^JGj#kWM`T6 zRo3X?EeocF385&0*V5VZ%OKsMMBmxaP%Dy$q)@j7>sE7Zuic;#KcaX5KjZQ<70d@4 z;feo*JJBl;F4FpZYqd)^X0noefUVO?wYLwGXgwGy$DSLqI=tbqgY1Fe&ydZ8pB3WF znTeJC52gnUO|U_@O%I#3Zrv%mq4OROHy+i-2g%IUUSsZUi={JlG1RFlpyzEq(eJi1 zAMUVBYEbqADesiT69}>^tOuwCnuLewS^1XuuLin#5J89t*Q^T`$1X5M}c00 z&RTuq_;QSfllG-t^Qft7h_@KtD;C8rBp~XuAs0=z^9pJoqQtSjU{10?ljufa@a#9E z?Y5T7e1*(3FKh-=!(x*ZClqlq5g8$7(5Nn}oD9xs(L^S57{@tO=e)Ayo^n=^4x`Ds zS#ohp+gzy~3jR^0+SJ!!`~6o2%#63PU;H-32~j9ygPCy(ey-h+c!GBhUik2ZG^S?u zy>n)1*Cej7u-6)}gh{pMFwxBtVoO4u3ux`Am7#tO)YpotG3P#RLsh8&5qf*q=j!P6 zs-#abO9CU?!0Z~1_5p>tyVGy(pYsD#(RuS|t1_W7zl|7~+(d9#5`@rdz9yi~EHBV| z<5_+|vMhn%aZucL_)_>i5Vx7iqPkd4?&o)6SQ5oF(wdl_zzO@h<;gi5$MF)!6dgTPFm zwy22jTyi9{_cC|&B`Xmhuk$L^3_cNP6zhF6`00-+bTIk(A@d;Smz#}irt9^~&vwi@ z?Jz-{JofBUK2LI6CJ0{g`lQwybH2}DA}ZwY7v`F72VX2vy=pi2_>InRL(UeaNJ*f71`iTkBh)qiR@|Ev$pgq}w|( z$P;B?y*7mizZi4*!1Fx6rB$`M&AW4}tIk4}KCFP-svRk>&&F|!+aQamsQjTt#~3Zn zHW(c%br{D8#Bh)|))m}gZblJR%BufM*P`I-*-|hsEQvjbbdo{^-$GAd2M&s4A7Lgz zrJkgZQ)~Yi&;Ig|EHWpr02hQkSq%Lue7v`TVXL?lK1szkc3UBUEq6Ofcq+ z8vRJTYE6^ka3R<4u}m(iW!)-JJgeX86c`Uv{>D4=a_`dlk}*DcQvb}w);^MTJ)N?g zqO`&~qB~2p)nIKZHr-115V}iWQDW;g{9bivl41nD{u1ehO~J$QD(+GK4hxujHZn3< z;JcF`u%X_Aps9^xOpzE3`mDcYv;Zq6#}?2l{(WfN^)9iNQ}2z?#Kd{RaLvHD`^6ET zKA$;W(#Itbm$Dq@1fv{;h-aUMDqS+Jp=LE7$ttpYAY!FxHZkbVm(zLnC1}6t^Vf7p ztH&$Y6QX?6KS!)?A>3R6pg|2@&8$L}92R1xhe-})cYD4$F6Tz&cA}Qe#WqM=hgs1xsM4e#W-_eAc zM6tg26;)5)HPU%=>FwE(rL6NN=%*iDH2O%ys!|hGThF)GJ_m;H8nb=7U}__q({}aQ z6~*7YCD|bIqkPXAczr*9!hVDu*(T*Ejv`N2w9CHsRXK{?uz41>lt&MR^#rr%tOn%` zPUz~gH^~ruRM>Q{%!5`gfMr$U=x8$z#4Gw=Uab+AP$5PZNz8e@HeXV&DWF-0u*T7z zyQ48ht*F=S_FGm3SD#hqv9EB!t8c6O+SGeY?cYHr^49r0d=-!1zbow>t~0uD7%(Ey zgD5QcdaHeFZ&zFxMOcoVhWm&MN-Doi;J=O)(A@fiddjoM9<@o>L#1*b$>vK3!y_ps z@QhT?Td`cs<_-GCV0oi{K0~G$liEW>FxHL?qTf{G))Z+{2_bvwm8^!cFnC;SBj$#6 zNzALp<&Q>{mZbF0AGY)f5F|+%5HU1bj(qR}6kC8_U|oDoA?Y>~)AqN<_Q|3z>5vp@ zj|dFg8LS4sa{B>3n$ChB3V=jrfeFgjoAL9h^-Mu>J3bL>_oz0{l;hR#v3bY)wEPi~ z_N>ycaHWxN?fZ)XwE*`^DJ`_73r&zBSF&8ldwA!-jXp*U_gZ8AqOK^^0UPXs=>t|v zgsrGfVX-j-)eQfawJ}-UQsV5J3Jrbg@(_}IyB4c4Ssk&t0iY8SOo&xqyNosP zP{@X@lGdNb;LiHjF0=)uzy2)nfXyygiG*QDTqqSVY9t_7&YP0GEQ`GcKhr)4zP_YEWSi3utzHx0-d!#{lzlg<_CxAb@eb zy6v{L8Gft!hJB~)+e(%GbLw7D0S;oZ6|mHyjET4D7eSCD18w!gqG)OT9Yb#4jp^Z! z!<^%Vwq%cRp5_Cjsj-} z#x^Y=)|Hlpo4R;^tP_=e{Er=%UV_ny?)^=Fck!+ut$qdeJf=M>o+5OmNR-R64U19L z)hG31pse=e3xEnp*M44ps@%i;kReuX#D8fwY~G($0?tGWNyu;U=fgHwsoYgOn1}SU z3=7t5=-b$_`SLE}Phe_(8wCs=9J6ua@c#q<-s_@UKG_PlFf3xCePkJ8L z9gi|x%RBBvBsOa%0b)Hn4SUnZY9l0=cFS4XR$6>BVqCYDJOuVyWY>Hn&EVzyCG02% z0_!qXW;8`BOgoFQdMIf2xkNnnjx@Jl>{k`&l<`*1fsN;~0#!O_)yzp79sN?35vskn znwd@r^uZdb=EhkZ=!qX}pG6IL50?}k|A&TMY4a@lmft&5HEI2D_R~ZKPXW` zI_N0D+?F3%dVhG{27?vg!qQ#+gGDGpRQ?zeHdC^L)VX2~+JsP88ao(7b?*onOV+#5 z9E{M1s2b90qH@GTFnY6zW#c*thQ@b-2y|^Z@@Yja&?eFj;5$c{C!fc;Sm2ew%a<2y zZz^wZiM!}A`*+6)MIhGY=R-B__HSu!#Kyvkya9wtar+erWWAhyfLPu02p0r^%6)Nj z^EV4;0zNNASC1P!5pS8~<@2>@R0j{e2YkM{#ImR2iYmmIbY1qBgzK|8oVGho)sRMc z>rB_x2abHaUWazvZCbqWA`{e`i*(+v=;o$_evyF#V0Ig0qVLjzP(jBhnipVpgHjK^ zpQ(mImZry>M`dXebL%x6N`4$?Zx5>XBZK3KsGZgz=~lT2y81qSu{gc)ZP~3T4V)g^ z!sB_=bk8$nW0v3#*0Q(LqCU6+yM42-HvK7uXSz%(pKH1wJWdw>#@rwJ0CabU5hl7% z8q_$Y-Ey6*K!=ptxdCh)ZO@8r8*N!~J^v2E;HVU$0xc4JF#5C1Q1rw1>^@CWZ<@>s z&0M3j%BbFxb@)T_veq}UO)3ao2j;T`u%+EHH=mNk|ihD_Q-UDpd z`2a2*zoB!`*WF3c7s!PehX^bQI=V5P7&+dGYjwr5HGMaIsfcu#Xhu}p1Zb1(U8CM_ z>oHxzNFkOdXxd)9ZDnd9Gt*B{ZI9b-^#yrvR!i(k{uiCMaEm6@*!(p8!u5CQJ&31V z&X{}c8Ll%AoZ8lJ6vzG7c4`VCZWS9(S3>N>iK4Q7pRr2_Ri+%K-nSHBs2ZdvCN#AR z0a_07r3Sd73pz2Amo(S47e;UQGF%qYyxnf;;LFJ&KM<1V+pV7t1U?%C( z{p!_;W|KY;&HY?*C+je0zO9LC7S+?7O;ZcMekc!C2!P@3HnBQwAyplgU>Qy>zGt=)tTQOB1A$aieLe69458mL98ZaCb~w-%u$BqX zGkZLRTC~Cr)i1JWcRHFC0+LD4OLKN^Mm|2*dZ{iahl>6XVq&FL$I9$hZiLX_+v(r3 zqROA7u$L5NF2x=`S+V^Ha}(@_qD)Cirb!+}0Yl$4B-wEado0x>0eTb1ZwA@^3@{$e zB3Vlbi3P`;%aW}2e;B@$egN;CG^jbdE3C|m*%dyaYrD0gd71cxy7^pzJI5nuv&(wU zTlH0VeS9C^FS^{TUvUCnej;Z|Gd2=!uy7a@r>yBt`BykfaMLx09$ek0c9DlRzPSih zncY|k*ZDfe&3*j{i;Ro{#*5W>+S3W`xLw@o0kC3GC(mRuB0Ar{$oo4RoL7uR2&K`n z76R;a0?a9rPD}wiELVWj3*N?z*H{^{dSfCul9CPWpTtycc!wi6yNL&&6F&bQn6#*I z6_hTu$X!UGc*<})AJC&`Oia%LVMs&4e7f_z3#W~=Q-t_cPpnlI!~3!hnwUFogAPk{ zquJIP_Fyd0gCDJ;4M#-()Z`&dXMOh$g2;&Z__s$h%v9#Z=2Mr?(XodRjtcpa=Ivg@ z%7({=GSIkmL$$%DTUxdP7cK;(Y#Bn|^+h&rL{L;40@kCH!DY{xXRYOos`}oUuYuTu zCt*l$w)moS*W+ZWSfHJNg!X~p#r&XOr8c>*z zQS&nYI)cmuK#b7c#>*F=?~111@!nb!v0A}kN%7fTD4)i7KMoC1bS*TlkzkDLn^+(d z-e>UL@0h*BGmVqxn#Zkv8Py7IQD?)}Usii0mXL-Jp^GlzJB7o@D@q!Q>oY_bQ0|6o z5((PpgF3J8K~H92ygwRFtrGNoyh}u-sNY~xddN?t{3eU(7va_fhr4HJ8YfZ_w}j@5h1`~!(XJB-JMk`6mFpFE zSnGB2BUroXH8xllqP8zX_%b+NtJ9{1!<1~TE#+|AL%*!O+`9QqnBMK)yo<%57zP)0 zrm{$WAM(+L*k3ry6v>)FXz|vMLUYEp%N{JNlVUUT#a#}DZL*u0%DTWjWVfztPNDZW z)+_bWIS_DzDqnWDuSk7{+L)4uem>f%74;#0%l`EeSciX;6 zX>K9o<%MP`01xS1XmHzoPlQXWpc}~9$!HlzN3HG8YI+kjPAY)u0?&DEpEdeC5of5e)YMjj(I*6Y1b}1 zgm}IA$bgCQOcwW<+?U8+da2C|(!nadPc#@wq$!+WJ$)0i1B&ym2Z4o{w8>*vzIdh` zhMBn6KgeMM%`#jaDvJHQ+#%8FNlJA0ID_7HnTgDPf0U`f|Np4^=IFS-?(a4Y8mF<7 z#u}`)^iet=xV0+;jHc=Yz{3{tM*0;~vCXA2=)< zp3n=N1LxLntG35H1xz+@ILHvawi!@aXU#4^R@ZDCoad6IZum>8YVLj|B2||;jfyqH zrAEakNO}WE;V#kk^}_FoCK~=jz-IJ&!`mxu;sHdX0el%%fY)1$j<^(G0C=( zk7eoU;8E%c*P(fN<_+_ixDUYPS=F%_$I^yj3z{#kFC1bDAILXSZo2w=h^q(&ve6z@ z7!_3D>YHlai(zW9;x609b+s^xL_*FTm?M}-gMMu(UG29Yuin%EOi&DW?STuvOe4;? zIOJPharDLA3H^mK2zreBq8IM|uZ+ZZYp&@F8`fH3)K=XI?Kn5Ov*I zp5b(6lBYOsMhe**3#FI70~^8JG)Af_48kf46Ls(q6F6n(8GtEOvNw0N6uHh+3%#o5 zQ2OX|??BQ{P@+ZTNZXQr=&QkRoXy=T@!N+a`EvRz>h0SA5a;Jh_MD8CiufvVJe+T4&y{xvmdde^3?eh4MUV)3P|9 znR~sdCtc=##x29>8rf8KE_>_2jaF0K3{Q`41R}12!RBa379z`LjDCw*`Aglk>e1^2 zR-zaN5WD&Z2h#MybM5D`hDF881wJr~D`-oNGGW)9p$C}JE z|M3Div_F0sg$tgGJRNk#UX!Qt2!{<0v;Fl4`T?adh&aq1-$k8y6%Z_MdFoH{cSj^0 zD`>H?-7I9^G4rN=g;`}g)s`jm0E3IX?^aYs4GjHud29as3g2hDU7N+{8P!v%d+gEJ z4Nz4x+QrhPIKc~JoK%ntPIaiJ+cZR*U_kHppQ)veRD-TZ z&Jlx9Vw>JcmMa0>c=X3GO@F0<;I&&RLSY%<2g_1k$5yyi*`0ov&4LILhJQ&a8@5bB zl4_p(Inr9YjbJ}~#i9mrn5)X~&>WS`ntkbJS<|13DX$cv5BjkkBjJ0ZNC=xnA>GQm zXgb8gW{fLLgtoaa@>FVATAc^wmV$RQbH6wi4lvlF+77EA;V^Mfj@dgPYCii5HSmjM zLFR(Hue-rAuAaVOVC2cA?h>uP439*aL>|zA)7o-l`cUt{5fNSF_X0JWNPcsYvn=}QYr9nhbQC}$&oe;3)g&i6 zF7yTZ4nb9w)M_*Zn;+>4$^2z)MwPr0EefweqUN2)e*Zg*%_jD-ZpY`xTaw-}2A&@| z_kC#9cbG&fkQR%}A7gcoyE$UjMo(RNcQ#@9Q(57uCuB5^E`4}U=CeNx$F)J*Uo&{- z@*JN?FemgG4h0dbMe!*~(Oz~9K1T&)taZ|;(J}*i!p(Z8f2}(=n!FSjGJL+Ow#*yB8!jV zPFeGz>TQkZGQ9X{}!M@;e4;${t%b)vHL7vhU8tG24pT2 zrrD+y`aBM^P_1bp#kA&?$c{UU?N?3VXQ}a^aS}cXhQam8uY^oIoCyxGwC&(5EC;WsRaRj3fvV0s)=1mk~*72r&2=sTt2H3=?C5&@G4Py8adEv6MjFjY1o!&Ziwb zBG=L+H5Z=>Zmz&S>7V(jY%!G|pM^8yxI955UlHk$5Z>x9%*mi%U!C(G3+%Q;9Igwt zOf)Wd5Gj*o^Dff)^HVw2&V78mj4>w04S_U$semWO-+YMoNgE%@dj_gI5b*L(;~`Js zfkb|Wh+@Ax8Nv-zeSlSlWcW=95l9bEnw6fbhiIGHM(g^(v6aSEsUQS(rq2)2{7lkX=tgkKsP;TmJ7=3z@OJ8ov8y_xX0X?3BMfnm!X#8L_yF+Zi>U3d$)`=^&*%K=$TTjk%m3(1xOof5$#$*rARP$>RY< z&%{Ij<79{v;$uy2dfrJ>X*N=snKAzS2Q9+vFIz(=(xJroR*ix9KVSHPLSDyQf%ed2 z{*NOP!zTeGoFpl6fOkdI-Pug~|N ze-EVp`9Tr^t2k({vC4m`BL;s788sO6p#QjLcR#{#N{S^3OY#nM=JSfIZg;e<{;{lG$_&W-5p zP}v3<=r-7Y-ssh~Qz2B79@KID=vbKO38aCLOY%7?6)!9-sFi$Qrst6<%-ygdt7{rX zA#Be`5;TJq!clN{e-*|Ql<1J;a#5iyZR#o(J;9rayHv#-&6J<3usodw0q+XtNPsHN zP+ivEoVJE3+gyRgOBLuY?o-sG&d=8}!($MxO)ajdmsY`<$BUt^R{i7+eYVqx&VH9hc?I$;~L`#)4xkJF7`AgZ>WvPm= zs&pFu-~43&^QY2s@&efOxH|EP_I@bSEj0P5K>ZMKoZHNkcJ_E^ORG(hFaed=wM24913R*OoGBI3Q)j)CIG+a^-Ht02YG;=A^ih19qXOAc4 z!ufkon7VVL(T24>Df-=t6;^Y{>Qq9Vrd3fir?ZpiT8Bofna(M7JSKrYbCi7^*OQ(J zOj$#|!W9!MQAD;w;nG=bbiA;>#JOtvpv?JfN~_RFLS22oxV~PAyuws&-V|WPzn3wG zA~SaahdDCJ#qXa|rNJ=RGMX4PNHU0H0WNagbBKY z8R6yJXF<&D9ewY5Z{H_hIuDI;Mk(3ned(@;IZxx^wkoMP=D$T0BLf<+vrdgsno%g> z)FEny%u(D~x<0+At<3*TEyuCf_HgmRcT8dH567Q*?NX*^5KPe$sAG^~LtBR+@Ny#w zxxIVJg}AR(N((=wVI@-cRcGc2dom*)Ggi4u912dQ6_47kI?Fx*xjW;fh@>J%{c}U| zubRYO`*IZKftMD4l3>i>3TviHVQRCvGjqP*b!B!c*gFcC-$_M9FetW)<8yzPDx^=B zDlZdfOe+C!CL=y)D!?K1Z7fkpZ`!gU2fxd|c~xovf|c5=wya$#=`V`(Xxq89&4}>; z3W7j(-@aX@N>E*O&1gmaqwsi@ocp~lLQVp+<1w;RU#gs`o?z?;Tknq#VgXF1VDR3; zhF#UgSm#0}$1Acs^tp431&E2s(QTlU;aEO=?b!Z`(QfjA{e-K^>5NIlGAy3FuWxYq z_9#r<7zfwE>vmx-rC7gC*5hnrQgkN~-2f!eQIut^ml|u3sYbLlfx=0|oDVI594Ad8 z4#s4m)0IgkT8z|EZOI$`yLU2$A5j6nrLe(t0xEL8Y{{abZJ^Id#=lcpzeJPSWz@H# zY15H7DJ&j&j#S_s~+@r zPrH|3vI6R<sW@o?(-hH&Y44~eq8K#D ztD0%tkR`vGcVD?zMYfU6V5)8jY=wYr^i95>e$xf;J|4iArTdIsHCtyTcwP%5fc0@i zkh4N&0lTFf7Zjgn>Pg&!Vlb?`ViFl$C{Bcj=uCjmq&BVTP9R`2@v#WM+&y)`)9N{fUaq;n=i z7ltq{-`U~7`yF;VBf+|l@!dH&?HI`S4^uJm{BgDd%i#$V zsox5Lt=sF9j-^1y&bnuD>R@-F-S&Mg3&H*B`OfvevSk@_mh~W=7bi}SVb#I>u3!>! z(W~{7)*DIciJ(B`19G0d=9P^wS%6o81GU#a71cjLB%p7&e}P%!QbyX zlD%Ixj8&g6J}lRp#XmP!-@d+(xjlcU3CLQrdUNj634l|*o4*nY5)Kbzeme>@em{6f&yi6AihkY_32vy#kH-8Uiar^npJppI zt`+919T0X!&!1^E-BzVIy~)wzK#RWkVOzc|Ik&$J7%i_MA{Sq7ThD_P>wkxbkf6j3 zc+0BNck(M7?73`MB1{{!Q)=9~uiX9By8^asr`8iCSF(g72a;YKXg!b|Q@BVOVMzH+ zhnbT#l)ZZPz`DQF3AK)wB%iyON%j=GWN~hF1j9sSfz^(Kw2~*P3S5&lc;At2=ehy( zjWuh-cKj1|H5e{u`V{S(!;Iw#=#LeZXQGpUgu_%Kj0r6_SrEmP?s?2Qqg-33KV6CV zbv-V+qJJpS#d=O?`YOU{cLD*-UptoVZ5dIuS}_HKStQt(|K?p%{Y0kCY#GKzQuS0| zh9AU?|Ch!Dn_?)M^|F8L2OQI)O78_sdG0)A?@>w5qjZ!0$E~QXO0DY2SACA8J;Kvg zi5DpJiQl*e7!x8M(R-U<7J_x{zNL*e2EyInlSARDVMb40jIuM_r!$?X!(axgv3MB7 z^P}-p_e&gegC$kGM4*yPvu+BgjRZ*{3$Xmk1pwgtD^+q#ksK1==b1s+9se09QCR*ZUZxqTU6Uh-(85kwd?sZd>Rq)!XmH7#%KHMg`BJ+#o_ zzuPXe@J{_0d@rP|h$a&bw^phBHeG(7+eu|Q$=U2in)dAs6!`4QDrL>H+jTN1GNf3t z?M2k7DKourh{jjef3TmE%{VHfhBa|Tf!mcgySqhY^4+7PJ3IZ}xaP<3y*UmD9d+Ph zFhAG_5}ru?N?42~6ZChI1oeq$5#9xPdx8Ya68S~4mU;3~ax=NF=axJQsQxvW6qY*{ zZ!qmbX8)<256tubJ15-JXGH3B-=Jj=F-~zs>zIKni#SK2&sPqjIw6#303-z+1Wqw; ziY)I{%f=F;h1pkzM<#?TPvRLD8OF&LydGaf>9%UbvpY4%PKxze*dE z2$Tsr(YEb8gv1dd)fI3;7e6l3J_1mpFZRt?t-{w&gh3Rux&!Nm?CysdT|WxiF?_)7 zZ=tH)`%uq0*CVnT&Am`pyE^Wjg;Jq-8^5mzh^9VBTU*QQdk6ohVUGUV?mUjvY?r_q z#$Vn}#EABEUWEePw@?OV(~g8aC?nz_V+2gEA5@mg=Zr0?dZwc2k!dZv?b8uce(GVe zALn{vPkKw8{wX~t2fy^R%(LN1fYMYhCzDr^Ct(Z=gsnu-;R%mKJiwCa|pq4`EJ#_gj_2pvK z7_YUrB-0@h-*GtX>(y+ne#pgHGdaQPjX_xzr6wt|sCbh+a|Rp*(CVYEAHV1Z8ke?Q zj})HLq63%J3wl?bwQYbm;JSOsNT-P-%%RwSPBr7q`bQh-%GjLFsz{3~upbfFUJ$gX zHoH@4e*C1$+b}m8rtHLPYski+O-G4pRgN}1SRz>YnCvd^9+%76Uo?uXsk81~je+Sp z=c3J$1?I{Mn5)=@tSFvapyVTgcY#3`Co>;Vd)W7eM*XTum@929z}v3wP*aTbr>UHL zHW_p_wp$6iE;8A`7E7#DJ5}dNUVWG<4R)ANI=3M8i0*>EL8RX*Lh*YU)%0qKC(A=qh2% z8*rVzCqV|!JX?AEt`XBI@`-p3(vqtgC9w#`NdgPK(&LPCqq5u!!^P*?l23eZ7}uAT z>95k^wLeZ9g-}JFmuG>SAh7RUj+?R00#;;&0^p8}_jhGt@%Hdz`{*M%Uv7q9wE3Xd z2zfn^LoZ`+u>S&-67Tjqc^J^G?NmS|>~dNCw}A7#_i{>C{wS;&&;5Sb6P>SQX8J(E z4e&M^zEX8>^^_1S$bkuzxGaNv;VeV(VK9YxL8dQQyeqY~Qsr3n3Y#MJYS$_3 zpiD@|-c&Ah3dU zt`>CbM5zNCGQnk4h(m*+O%%fdD$>g3{-*RGW2WrCdv+}U{_%5QYb*a*;|$h4N5{fb zP-mlSbS*pK)|olsq&7jN>D1k_V5?ZEwf^y3Da~Ev_e)_jw+oXB<$;lrgp+}2vnTl^ zFvAfX+S-|!nXew6m+LK#)FPw%SrQz@6|*Hv%7vSgqR2KK1>u;z7hAP>`ii7vEPcRQ zGZOQ!3>Tjt==T!Q<$@MXr%dFnduOwJ*ar^rE3lRE$cPG-R3aUCr*!4ZU)_B@kT9(` zvVkGFZ7icKH>^(&s<>u(s0)anL`M2kRR!Ok4vj`5WhXPo>n2r2@JN;@V-;uq~Spc)-91>+9m11(oa~=fmGM9F=C9b@}>2X;Is{s=K)V1Wdft? zh4tbGDnY9#H)S=I$2c;{1kS#5SCVIIuvNjCTo*-F!y!NrhXm9;jRGk8XtF4QHS;cf z=(G)0_{FA}z*9Jy^QMG@h`U^BpTHuehY(yXp9|`f+DVpSgvdnDIO|FzWExml_hb?@6lLw&8ne% z3@9!!!zC+NB>hZKHD9d*4wV!3jN=K?W?8xYB@TlCRB2-n92}h1oxq^FV`h3I$EcKC z9bMd#xAlXEFqRf&O5t+v5Nd8B#Cnv?AQ`c=2$oxz>l!Q_|4|!6F%OVP7YDW1;&n7ma}CzM&ZUOD2KetH6$s zcZ}*-1_LM>j0=9GU?!ibE>B~lX?<_%<9upX{5>?;DeO37rw5vC%ZI$+3H5u2 z$%bFZ3Vclo(l18Oc8;H^41?*#kGspTRRephkWvsJC(Op-8$%c;UYI<*B-&TK z8k#mM-p=b9qX^o_>iNdrwfU(Xtz!u=KSI%{Uw4NS2$IYYdXLeBvg~0M^#>u2 z@q8RV5Qbuo!kn2-R8^v;f3hI!&aT~oVY|6fDn%-$u})!%&fYN@3uYSZ$I`gWJ-f8e zEA=4Hu&b}MpqH&N81wcQ$@orO0~;&DF?89i+i+lg$4<6J6mtPNVKl~Si;mZ;!!vHW zh{o}@#2Aw4dlp%NvQgy4a@fWB+pESKv%Ou|_=KbBdj9x)(EVg~I*~#e3EJZHLoM%~ z9D=gT`|x0+mmXza2#bx6_owx~2_GI#sW)a`h>gecjJt<6{KRAx37#bgYhd1S1 z`EB&+MXCbW0quO^HqS8Z_}0?WqSocjZQL6g&zKvbay2kU+XYi2wQwr0@m46vON3)6 z)s)On>YfBhm>a#6sKo zcmT3uB2uBK*dhi$gQ2LN1AA$sA4o|T7foA>5X)U34^=mxbuK*D@+e1}pAF!lrqiJO z&<(M_5vQr36snnrfBWRD*KT6g2dRZV!nV)D3w6X5RhZd$(gG)@-3F%V)f}SlB<|`J zX!Sv^{`zFF0<)MDasq1wP>4)7as6^roDR-YD|Ky0md>o{hOBR#o=JB$A6$2t8zbY4 zQ;ZHPM#+JhDpVql9GQP5+4}O1qf#JAp!?g3tvcBblk7=)&jikp2?+`Kw6d1WgZ`jF z2!3Z(UAP{P=NO2@d1c1?rFPR&1_jI^&i$H4YLu)~g2CNeACIgJbozE@xr%?#X+kr; zGQZx`MTYK-793Ofxmp$o^M?Y!SjMbE^^o%4Y% zTZ85Gxzvd8Sv0X_g*T*Op}Rpy+ZCZym6k#^ZF#pjk1;~_6ZxfEw?YEJ0($Z{IkeWI zB5;Spag5Ue!e z!w3DthJ`Rb9`GM93m@UDM#kSe_%9#@{NOL_8&=mtFy;AQFiQ$@dUqTd8ZYi21{E+M zUnwTRzjD<5SJ`__!WYhHgZqEHK||m`lrn@xx#gh#i-WNOFL_h;&$tk%fpLW=rR8A% zI}C&&P*Xe9p~R`Yf7}u>QqC?_*#D&8d|Ze*1L|Ch9Ai@d6En|<17klfN0a!EuHH`A z7uCe>|0=)xVSu6Q|3mrR4*`s6K$N>K;eUBRfc_g)phjNAzyC(p56psPh$>z9KgMiarIZFr5@bg3*Hjkw-xDH=Y4)QG|FWu1 z4dpy>;ni~Wx|pP-pedg$k%}2bgMBlX-34WH^xhAbwLkZ!n@!*z$>n3i2ep z5b(G{va-<>-XbF4C?+*>u?ew%AAr1wFDNGRDnF5fLG@$1c|Tud8w@Boi?LX(mBQ)l z_r=Cj=mmjSGfI1pz+fan4Hr!XJ`mt3!0YsW^~QYDwa+_EG;jB4{)P@oL>wvl3hPYu z8iZqJ5dV(E?SZWbs5t)1b4_4$ApqmILKTuV0q9scu4?c2eD`?QlMYnELnG|ZLA zEDD_AgmlCAks)JHnJli*t$3 znyuBtj5l`_G9KhN@u=+*8=k>k+;xn&Bc;a%@rRh7zAcB(WKbGjF5}gftHQq#Cei91 zNB~ZvXY6g62i*?}H|lpsgzI9rwp**X;>tRq$7B&r`(3c>e8a z>NkuS;y$3Rdgg8E<%ZYlWg<90g&UL|j!d7={t@O_AbWiZ^(t?^p&3~Et++ox2hnao;|?_BR=HdLZC^s{vNhhE&uH z3Y@}ik6fE5;Iv|CzddRhexcL6dG@pkEt||&Z{%6&q@)KzF8X=x-a3;Zp+~UbtGG2@ zdc*7)AD!*4)k4Q~J(FlT_Y||e2CLo(eQ=0%MAX^RXZTd{y#-O3cA&w08*DM)xDBIo zdAI$(5pWh0kBNuZH`(iCeW1o}Oe^+8<_BY^Hzu;V9=`EoYP-)MSpL$nY!7o$Z}R}6 zCjWqWd?~>?sM%R>f!XQ(yNgzKyjt|>ct}|cGxN$rF9NNjTkwg|gN<>)EY(0h(-_S_ z=){;KasD8gntD8vYF?u&r|}UqIPA;tR`r+k61y>rAFj^<00vfpSD$4MDG!%5@xR4_o^mvu($bpQtte`f-k7men{aH-9~7 zUI|L|=Dz#Ta-`!cN&ChyTUn!5-sDz+sg5NXb#7HnIvBnoz7+B?CDrgbhvvo^bvc-% zy7`qRtEAY3FIOkadOh^$>5p@*DIO14izMUpA?_16?OEdTP-i-=#%8=P>sR_dPO>3k zH0^@#;t8dtrM@pq$;%7243^!|`D*O5W{LBVh0BMbS7a8|P=7!(vtvlx3RF(y70p#8 zU``+s=Ct87?K1P~m&l!;yzIQvL#3=F)GI};>EBA2#uyrx&Tf8>l>MocHY=mOxvoQ@ zq$X);!EJD@DXkAOPZi+mOkuUj8darY!(tuKZFMX;;#^p}Vhni77l~9rrBcgze;(ph zalTwHsc%RHuz(n#R_3=_GBY##eQVnS2dtTB=GQ$~`50>ElUYfWY|L4sC~G#ckeY{c zn;oc7BuTra{D@z)2u;jdQ(S+Z9^J%;PP3s8R+h{u=ze&$63 zd=K92Tult)h~LsQsZ&V1c#%F-#=R++Q?D{?+e>Cq1Awe0jDpDOmgkM&R5mqpUr1P) zRq7PmCXiofOm++`^uA>Ihb^_W6YF@9TglTuyL5Dgg`IaRAQMI-RmSF9l0+8Ekd}ZZ8MPiFhB^!J`wbTji#Epj%yxUL9x_ z84ncD-#}%zcY^U$&R!NZZD4ugFl(jlP}%RVLfkqy5=OzRbn`J8k3Z6$0d9llG&jE^ z(^WOjmPkuzPo#BZIiZoyx=w!GCBRS}G$bCMVDL9BB)id5tX*FY0Jy!CY{7d(uAJa3;AEm+t{&KHnU8TuEX5k(&^MwR0@s@ct3w-NovDE z%6Lg>J{{91VV<*b&O1B4y2s$gSW3Oaj-r(oE#)NM8;Q^UCQR%=ua)f{XiNSr>S8U_ z>44ch;a0iMZY6O-jgc2D+2oMGBPj&|J$E84tEa~a%Ly$=>u1}?;dt=#ZZa)sQ0C5g z@sAILHX2&mp)_WTAhi;%ffgk-kH;aq(|Pjq4~n}L1L+L+PWiOWukH23Vy`` zft#!Mo2A98zS+++!<9x2ODslBt52w-0|UJ`W2rf5t@Vb*20Zvv6^0g5iV>J!si-4q zxb9{POG|rM9+!UW1%huxL`26+t98F7jc@w9zwZsYeW1dF@&g+zQMb>e2J0s&EQ;uM z5~?JrR;SPsH5F&j;89ZjOflz;-C|pxDWq%I)r~pw&DS;kuOq`$YS2Tg>gGClSvT+| zn5;h`F~(SOUi@>MvYT|zSZJ^Et0|M-ep6p7f_y!ckNo#geTAS6yxn5gx0y2L=yd zn@oq=2jncHG?NN-O+jvEK8nh}ET*^%byRJF`Sh;;;qol=he2JR)4?A*P2WmQ9;`MQ zrTmZBmRHQkzJR4z=VgaGI%TY7Gx!aY)J5e__qLDp$Nd!uH&NK7gXvn%4j&|iqOr#T zr-a4K?%Gru`0{BZY2z*FZzGJ5YCq7aE;3o-=hFw&13|04~3Qx35kA4&`=bAlV zNK}0G2PkOnr;;nrA)cU=CH?p)ykh44QCLwjYj9&^Mm$rJ&~jj(prO``VNB2dcA|?` zbKgI{Yq`VmC3zUs~8tGZrZT$%S9T0 zr>38BSx=wXbevbDs)A}VAWmvzd|YXg_tO*bw3&U8Y_qDrYbAI4kVu$x`E<2q^Fj?X zv^#FvVl-7Iry&LII?+%V$ZW3^6o?wEWS9k{bti8e;{?G>Gu>92#I2R9(nYj;Cbmj?+`htbcSzY zbP~ZPCYkt+w53a)*}be3JZ+C9CE~u7>8_^J1`+6c5X$wVQcG*@2Ap9;o-%vnmLz z`l%h@sKoL8N&4X~vC31pq2R=0iO4JC)bgE}c8{OJt)`Gr;X zms5}h$^HH%@Qp~5%oP^2l>@bAK`d)r>^!i1+hLTI-98^jJ_484luMtnM{>L>H#+Vo z#O*-V?q5f-Z(<0ks(A*8sR6shiBXDwTr@=uw^+ueRyWZEZ|AY_tuhKSKccvUYpA@^ zRAXrBDo0mn>RCGcJQvI*z?*hXX@j3Vq&qQP!XhjT2m1ijfxJ93zvqfYa}}wy58fJ! ziy`{r6zDicx-L$_+X-hPN{!;VEz%Cy<-3eyvxS4v@a>SI${AIYbMUu98wfrI2Y3fF zVI{8%&O=5Cn-Bx?WCsFz)VaEHm~Qdj_eK{Jn7e6quz3qzgiH; zRT(gcH;b1z)Z`jLz(s53XiU)h?1LFv>7H(MYV<$EO`MbPwd)+9oS2opxoM`Y$jw}X z*UnO}9(-|23mxuE$3WpB{EFtcES~}CMjeR5vR=z0d$|Iq`qUo<_Gl*7##hc(Bk!m? z8h#^kgOe3#?xl#Z)~tC*np9^ZzL}|M#JrjF}4xUbINnNOZ z+<#yA2MKy{5;2o)zPuN{3{_w75EOg$R&c|x_1BPlt|wFYgh?(!%w@nWqf#{Q`wl!n zr9L46Ipl#sNjQ}&8iTQD>y|-s`BrtHaS$eXppM9pTRmJixlN1Z&ZMe`U*U~9S*pmk?$=5Nz9Z0_Rhs9NBjwKWoXk~%D=U1J;4}qqRU*v7pb4RGGha}`0 z(;kLR@{D>{?fozmV^DeGEWlvgd@Y$=mOqkNEJLg{jp85zXp zEYJJ2?(mZo{otr(2v<;25?aUWL0LKWv26@Y+JmG{vC)+i>`}NJ*Sm`+el*n&t z2qq}D4`587UF)(FIcl;oP`!6L79nj8q2o5Yfiw02XB?uM-<1YggHXwaDW`F;Meaxn z`W?2TXk{roG`uoO<=oRA%zhn!yH;M-fn{f~jZmpwmmP$Yxn7q&IGaLzlm_4mU5qFF ziK_VcTHn~XYZ%dh6*Za+7#5d+JUnx`EE%x>;B*KCVIZd^XJ z)7dm@I<4GWqEqgGdWwHC2#^)f*IpaPOfM~^k>uJ(TjGIu@)6{ycZmHqvk%(c@w8c{ zk?-}q6I@zYwoLYAu2?o2&|3q_;rF!VvE-%w#Y4k}*Vl0Q$}w_v+Y_GD6T%q4rqX7- zV`a@gAU56vY5s{$O)9%*SoYLB#tdg6T;_Vo`-(X7AiQZOZfdr^+fbBR*TPdhE0u(g zt-j*ou*5A(v@PAOuyTn*1$7yP({It{vInBqScQ|_?t{+Ur4!|pc9XofmO{TOSFqT0 z9WaN|N_J$N9527iL`7bm>uL2Zql{&HIt*5fQD|m1X;F#i(mO~n(oR3MdjL(hTr+XQevBG{b6H5#Zh0|fQP4FB)pzE+ON*-UEXx4_ z7ZX)SbIEP5$B%sUaP-FUdnWh@r`xrI4mrbnPek>NVpoN}=$ZtT#j+txd2a9DX2;8!19s z+b3$l5#HaPiK%LcDPLJtnujEM1RlQw$LXStZjP)BW&_P_EcsX7qM!ZId$i&%EuT5< zg;+{$?GN6cM|l^fZlx_{8`QOxzfF(yQ|O@e z1!C;9h|=Q6_vrBk4PAlB_Ozp9RV}~vM0K%?D!c3qKzmQn#aK})G{XK|cU^X`k)oyv zU%}AI6n-v)ni}XnpW0!depPNX3{S+bcnpq}k!AE=oJu zB1U?%+JxK7ePW7UBXqT9rLB>ZuxwA_-az89YLqO_@&Uy9?$Eh7%~DKeW`+0uYovaS zyK*QAirK6Lpl*sMe5H1vV*8>e(R00ki94riC;3eHGkRfl>N&0TgdouF2fLexMO81t zB*MT!?MP?(S4i5uEI#BYjfyiNO=y13v4{Ngl@>Cm#a!u?h=>{y?rl4+CN0H@s)4jC zA3S)jYu{&2Jb2zm3-0({Qr$%p%ZK3GULD`oA`&KMrrg|4)Iu;3q&Keh&tLnA#~AZCjSto~Hmm?P2j+P8Ptf@l*(VF&@ygv!d?!+jis9^r zLaVj}(j)Nv87nX_Fedz)XyNR*wqZr&%%r7-Z)ZL8tWlki%SyeT=nE8YS$wC@T1u}R z0bh6>zB!{F!3XoygJ?U6581-_=U{oBg5Lkj3eSIkRNPE84mM2{e;n6dDb-eZ>VdHR{qOYSh~->XduD|ZGZ5m zB?S)%_jX=cA4qYt37_)4vCCkA5)jAj)f*laZT;IS;_l_L4nPz5gwjSg}?58^u*MU~WVZ`Rh6T(Q34zVdU(8&cSXw${3bM)Ig)+fjf@oQQfl ztff&vgcMv2)=}Bem7sAcd{}HrN?g+6BrWX89E0s%Jp`RwjfG2c=v86Ec{+1 z@Ca3);s$hF{n2+BI8`3M-=nbR7~L_+X^3k|BZ zT%Hfq&P;jepm4MvzF*UIX%6H(SXtCotKmEILD`mhnrt1+DBs2G1NFcuZ{OhFgsQGD zpyM14)!$Qazp7gYSkz~a|^Pnv9u%}jWFa_acdi3h7-mlR- zE5M>eseUiMsWsmKd}p)*-KURP(XY1n7b$A?Y_ulanoQvir*5BBw$VH$7{`&UVTNBRT~k-9 zgK$qvQ3<45u}Fqb1@QoDGMMU+{mhF-&0D&A0_{-q)J9}!hv-Z$1@z>u95mYFoe6T%kfy2jQH8>W?bVsqWaQvlpM;q&vZ$S`3A~ z)sPW>b2yr0_Us4bdBkL2NaPmT5@G`0L`g?1?d$#@Zol3nH^a1Dq_>`k!xmphi94<)r$FJnrmKUFw2-Yxt zNIVhMLF(C{qfG`0X>$a0lX9`cO{DvW_4=+`reFm}n@7}YRnp`~`l3dQCayK&H>-Pm zDI-=s4QlK)13w6S9^nD6QU_y4dbft@%W@+RsmvZGh<|^}OxxPXNv6^g~RnA-|XV!@e?&2KlDq(6lvbcZL(~_>7_Q`4Q&M)?3ye zyG;Eu#~ZZ`R}*YPhUIF86HwtQ`f5{uLMk#1|2skTeE7tPsA%uzRRJI$b+PrnYV=?v zWVBGX2L{fkM@oDf0+Kz#U$Wf*C^{ z3vO3ToA?%+B|gL%Ie}w_%Gqs<=@N!iFw5oVV4Fb%wjQ@pOiEHiX ze2ZMp3eGSkji9yz77JD8p&QCzBKlry)c27x;v>q@y z-lexmu?7xNB1Hw!ali?yAU6xl#^hAb&62Ik8oQ@52xn{4hI>Sb z5LiL`VXktp(X<_73GgYEm6gBV*^o`B^z{H=UIGSF(!rIBy!JyYBa1{hrKPnGm6B8l zaqxY$fe0{}DIr|RnH7?)IkSx>AitW(5KI}7U$D8~;X1Eg6(Wwt{2AwdH`}0u*0QhI zcR((kxp_m>x5?vN_U<1$qh-(Y@RQDFldAz23JS_|^V*s2dx&<^{g$w_w6q)ny3gBT zm&leOaA7oZFIq*K{=kyhaN3nSEyq&T9d36)YCcS}Q1XuE{do3e5SNfpxZCLT^5{Bx zd)xZd`P>MDwRz?4G93eIH=hGFKE&fm9>nV>A6W_uThz_0t>raj>dCpiBqC$Q@wq_s zn9Mkov~_oGNZ|W|;FMy7QD4%Mjy^q{wS0e(eXAA_)41dU z*89FGClnj6rlTw%hm)Lz^-lOl-4hB5ig{}PEC;=2km(ODXR66c^uRkIi}^FVR|Mz9 zW`To)3HWvgO##fy1*QjP&Kc7TY=okUMNxuUZv|-Wp{{*$Y^dsY+1X*eMe&v`+s8Up zA)t5qH$mV3$JSdw)s-~i+JTVZ2`<6iJ-E9=aCdiicXxLPn&9s4?he6&yZhbDOy-;W z-+!&MfOR;#Pw(#C)!kK3y=@V(zT%aL0(V1&`f)b-vxEaf5}+X-R(*CzL`R0MCnpz^ z%7&sROcGR+37n})PyCj92_}up$_Wj=9F<+gBCTb>nZi@y-m@{)c`obt+1#)@WhAN$E`=*7}HE zbDGR=R218FD^Gt1`G4Mw?AXowXhkcQk)9qN#q|KuwFv{P(jK*gJ2*sfb;0tMzpr@n zDW~gZ3gCGZ_lHg-nlVR5cWXgrejlZGDIPlb z=k_-gdVWB*GT}OLh103#NGhXvDF<|7d zJa~fzA_39=KzWBM+CQ{G+8b?4e^jQq)08`cF}` zZ$(+}R2ly%isG#(3PwTJ{}qMX2Jy`XMeBNW>K}Z9X9f}2oz5u1fA|-j!1Zasr?k?0FqtpVsslli zf>U1?k+5oQQaYIbrYQ5k0nLL;%JBJ57wUjgD3!s8K2+-UZvrlfH}nC=oEMTCO< zuPp$6Ao`sKBRCvW^`9`bGw#IFQYbWa!#kt#OxWTEU=;w3LGX`Y{a^ik_1=fKo(0I3 zd^}rweO`P1_LW!^^u2I)Qr4DJWP=sEK4pwGfi zPRuVCN$o{AvUol@JlYlU;=sfL-xrRKh|~o*5II+jvOEg3f^6YWQH+jI11>Cd7L19{ z?#I2bZAbaB`Qvy1K0}kXPK^ypvxmi}^T4c^r4EngCA-ly@UV?;+syw%xCv;L-v9v{ z1RA^DBp!S|uFoB=wmCTezWbN%zT8bp>|sotc8J8OyJce7w@wLm1buSDJZAjLasiv# zn{pG7&gH>o+5UXfNyceMslo(Urq_!L%nh4SClp3mZwj49?#1XG2X+}iM1d_@p5yu`6K9i+={CZ|b2@{* zF>%YK>o#=y@qos!C&vkZ+mI$i>cTRCJS4z6!qR&E^I)R!frX3FsHVf_gc`{F@A=1- zf&>O-A@cZ}YQdXL;qiKN8F_s(Nd4cC_-n$jqs*v`#!VM6Z~sj|7s5ZxAxJ=mWcf4J~vZ%8p=`uGremCJN%#1 zBLFi609U0LfMNq3Z#EE_z&z^{P>>NpK!XDn3ad1(Wnl*(Jnsr^;{Vkjjv9ouFIJBV z+fVw9G%J5E#sf+xseL$CipK6pY1w?tRHD(0((HK5=jP@nkrdJe^pU?bE(+zCz8a(F zle7Dw+FBQ)G=~qT+f8e}{jP)ujOP;vLMcPfqt6E65e z7=sGz=;-Ky5#X0FEd^q{>j1MNvrMt*I5>{1q@Gh}G^uK#QTkYUz2mWm&4t+=!RHv3 zwy}%9GbK*<7fFL=;eP0}+Efpv04}BD?DQ8&qZ+FPcip)d_bBu7UDWf!qJF^M89}z~ zD~M9Qbf^EMD!#^o+BM9lbOd@=NRv#!zOV~P+Tbz}d}yvW?_|dToo9QaqP~mz$1ODO z3t}eaqardXvDnfvGlE3WBme9B@pOI&gsbTpeOk};eF-&t%{cnwTfU~JtzChdzUEvD;~xb=@C z9oGnd`lv)78MD98_I35^XyC5zh;+{w^$)?tAsE7Ja5o%{NQILtNyHl$V~Wg>5wlbOVcfk>)f)c4eg6{hlSZ+ePQ@v5c)$sV?AU*f+wIT5Utu zFaHo!sezWgabbpsCFD`tsezmP|BTBsqKT=g?-T=o-~NX6<<{$Uo*C^lH~tNtQ>8-+ z0}K4(Gd3pGay_SZAWQ+cz4fj-i)v1Q-yf-)_VAAx-zHf)5nM&eF<)M{hbxm7;1N3z zhUT`}6z~A$A)k5M_X9o*B{$COU7ha!{d?9oaH5^vj{(QiJ7Py_9?^)4xa@&`iCLeo z`u()8Bi3_98AR-OI_vmG!?*O5MH!b1yX+!;>OhPYHN=3igN3#XWu+6@?S%5wh~YBx zzSMiN1`Y<)#cJ;`;c#msw~yfS7BdNIjeL&JqbZ-ydug;FVA$W6+P^$-{rD<=PC8m8 zZliliZ|vxx%F$fGlg6egu0l~s?5q8ClU!d?Qpk2wYzD=u|HvKBji2j+nX==z!OKv0 zpX6qS8c4F*q`!onWS+E83(;SOE|?iq7LOQNPsoh^2g??GGuU@co4MI0r3O4qI71UL(yyn?hbJJNP58n4Vn=1JAg7rKjS%ziP5N*K%(bMI7Ver? zp!Hvj_#KfU;)ASQfJ{$*pAO&u$pAs6*=hUn((^Jrn8GPk5k}};8S5e1%I8_)nE>$T|B2UY@#++2? zh9-mp4h@klTOxCtD|Zr{diG{j=cXgr&!4bo&moVy3vr!HJ}#|TsuQ@`xWyhDI88D7 zb|);<@3^!SZ>Hg&Fi*YW#X|(wK8G*pIr5!>ZOla+@6RQY=<=JBW0fmDJ0!`833D_3?_xM8iiHf;`_RI zi_HaR#y-e3f49$Y7O_lE$GqBqa(QGU&)JGt=xX=lgeCMRRXP7v!+HY=Vw>i#&fCk6 zbE6!`&uc%aFFS8*4ZrX1Q8%Ro-LHn}*Lb~LX0h1sMVAp47Z-Qqyxs}>SM$me?&zw* z6XUA?{I~?pl+(Im56;W}#z4aFbEEQF;os5Q>H^&|M668o4v-EQr5)u+jYLVCT zp;iL(n!6l)m^y{7BmZK(RhF!lRyih%HIIq0@i&3@WnUQ?$=<4oOt&ZZwhAN#ri-CB zx?FI?oAedn(vWfnWVxQs`14CkCGYRHqL3?j^6?ENkSdMqE`Z=W`Q~Rot@})m8BTC8Fx#s=8NnyKi@5GPo}nK*Dspmq?#O3NF2a@A(qX3_ zbrcDb$C@U47V)J5H6Im&j7e6GqkahmXS=HNyKQA;HCxyqQ3$hfn%uHx29#J_8))v( zEKQae{;Vx8sJb82q-3cf%MWy{ENbc{Xc?akQGI{8BG&s&A}jjbgJSNN?BVY>B<>rW zsHmuE+j70tXEv?HGUGMm#W^syqm@Rv2$DHI7^cuA&(`Og<u74$sBLkJfuTYYwpE{A$DDrIP+K|n*keY%_P5u%5s zrms=id}>Ih)GYhkddFa~{;));YPHH`+fr`mD|#=xnr6V}Dt_3AB!BXBPTQ5uG*QDuY zEWcJ|6O@dsiAUAYse&Yhn!-BW-lqE5O~y}UUmeL|k2aq#8h110+SxCxoU{2OQ}o+K zA|HQ`WfU8Gv2@nl zU@Vqa0n|8m)08*Ic=>J%WQMm9x#W!YS8VM1cN<(4rfhYTM*@iR_k682^JpDCEv%y zw{l?9>p*I$=n-jzf4Kjg7ERA7Ov7Wc3Q3u^Z|gp_q;x-(nHKaTF;g06^QTv|kKH$-+kzY^R>sq=sX_z#6G>i~DsBWRKv&|~fqt^M$E(TThH@;qtzMc& zT>k)=SvauBt;rNbnLS506i!^EB!jIc$fWhS_Ex_&N|g`i&`8JMxQLy?c^_}`%hnoo z%nj&P>~>1)T?p{L?+LR!xA@pISUE)*k7E%^xe#lBBnkzb#Ys<9A%``~w$!=j1H^nwwYk8>AWf8D=S1m&5GLke+;t$@s_^_g&}b3Y%Q< zW4)VS2Pt!oDy^sGyIr=ZsOr`}O=O`fXhh%p2R1a{3rH_%IOkrd+?HY~X&D~%+pi~( ztcvv?ooWXL{7n7wUwRpe`~>7xF4s-cV240L`I<~Qc;89U zU$EmNvqV5-{YR6zrTUZJ%3=kWG_{S_xGhD%;lA?a$Xz^`2ren88_w`#g5hF*?cFT> zmhC5W<#-3?pV$ppVe^n5S{T3$+_02?j4FOi@N>zwesJ;IG{hFu5=Tm~{QdhH>qlXg zG=5S<%t^BCrle=yc?aOt!A}q@-ydk<=iFXnAuFu#RB}2wD)!+my8qmJ)_TqQy?g{V zK>2)|YN3rDj3TK_C?2)RkUM|^w9M0hO&^E!^#|5>yfG+KxCV=UkQ|obvke;r8qP_# z!q(iu4P8hbRhSLeALP~g1QZFXON-8{Nk(n^5Y;;6A6_5j z;j#v7Nyhf43lHZErc}4YwH{=-OR=`Gzp?$^aP^y#Kr}J$(;)34AJF+-R`9v^6<$6) zgty3qB8CNo0*4Cu5RnS?e)bJI49NH6%|W*0h)HGIyH@WMOwa;ycKI zkXHdZvon#UcI&%q96qz?)D(4b&0e4S+2-x5TN_Y@N01zT4W5trdaIi&j*$a|iN4T2Htr)1mRc8Gpr#M2RAu^=n6&xEM5{>&Y?IkC(F~1a;)l z=cE!*8UT_k4mO#iYLcf|85Qb98gv6;Z-D}7`b3DVc|_QxfG9P5q?TwvlVl11+&H0v zdUO?eJ1-;Af1efMnL`>Q%(A=I#IrAm3#Hlx+4(Yr@-idw=*MA9hkC}NkRAP+V9kdTh(nCi@SdvNY&Lv3H5g~hGcttE{?!yr9**mTw1newMV z5zo^*Z!mmeo=?gV-mt6yOW{8pwr3oB#7RC0c!GE&`U`RW-U)YY<KC9> z$+};_>}BpCDz-}U&BfK;IW!I%FnHa3p*(N_H8Jk_%!n$f20x_uC?%Gh-)p1@)|09k zo$>R9t>+6iA20qK0mKCwX&8ef=8x>yMD-lZs<>qwVKFhNw@it#vrtB_Xmfe{Tj4;} zRM^Ay23BbCrRzmaznZ=zij|=Nigk9X-j~I0{X(*cRP9-uSzrC~&ru~NvL#!9ocjhQV z&xjnP@drFUJ`f@j#C3clo@hE{bbth}9DpVFdZIF`_U*BMYG^_TJ2#MMy@5Kk#_RZ! z?x~4p`5W)H*5Bph>InZGu>kQNu&bVFzn=Ooj&1w>TL}T|V*7;Sl)1UNJPG|{YVMJe z=uXDd85##@qFz}i;vmQFNA(EiOm%Oz+jr)J8Qi_|{=)^Q6LZ{-IO^~Mx+ac6%N9CV znpWgpV1CVp(^5)7sp_<0FSO<$H>k_7c>oK4WFit7Vbr2 z`~H{hktR&mI&-9Q>-7ipI9i=2WVE#O;7PvBUfio}eRW*KkJ-=d(nSN%tZ4%?z3HuB_ z8JQ;59b^eAYx33QLVwkRhdg#$*pB3^^`@0Mkim^4!B($dl}!4nSSOou?8y#0okaEr zY3Y#ut_t}RQFkZHwrW)0*31t%G=sDNcGR^nc9hYpN_v*a-NdA3zc(i7Jpo((YBAb& z#yKf6be221i(Qfn*~*N_4d_hiD-$)+M^sKulFGsZBjq+lIr^QA7w~{7$}Y~4qbXN& zO-C~93^pl;OIzx!NLYp?P}O3T`MUK^bh5p#%A4ruI?0$bsW=QuP)VbO0mB7*ON&r> zL3~v%JY0Zp03x;H(W}HMC?i003zPzccw{s@&w~47ZWd}{nhT2u6)!Ls@p{2N5@pL< z4z?vfZErWs!JuH2@Vfps4*lx#0z^E2(cCjkzL3CeSW0q3t^D%2+;^b()8bUS5b>(WpxZM}KMwoJI(f#3xZ4RHoPq(SuusZ|u;EB5H+XKC!=5+5yuduuW zld_abtXE3(K{xs+w0uL#ev6}S;S~D!YM9cz@BUKx?G#NXLf2(MX1)!Y!6SSwoH}i1 z*y!*u)gXxxcUr$1b$I)}E8bguSNe$9BHyn?~veEW+-Eft0_ z`qPD7d{^VDK<5tX#C5h_yBRFa@pWIVEfrrJe?v&S>l2s$gK4Kwfo|x+O2@UFH2*^l z%nHjfh(zKuLkXhTJx%t``a$%3E@KRj$F^*0W63a-CemTJquA< zONn$ATGG&j`Ga!R*&;)rh0uuNhC&W3CgzJLZ6>up5#5}wl|MYS0~kKsGTBxn`^h)d zekRp?0dbir6i(mFI)Vim#c=cHdNWl&J0N>9Ga&xQT7M4L(=W;4pG_*Gir`zeJk;Eg zM1i#-{;S31X$h%P4d)X6(eEdoW2lodqlN+c(SYB>oTf8QeEJ%$1&s|Xy#4q^eajp- zL{#uK6qy2dUMMb6ou>VX9!So_#u8(I{JvUksdK76_TBJux=g!HouyYw2gn5vjB0R7 zgc2tI8+3|8y1&PM1DzPgv5N7uN`#x|nMD?Encc6UI4%E1=+GsvN&Bym)n=~8uHI>) zE}2GZe&%lvnfaZavpa$YbPqfst?@HQDa_QUd$;E?^`=o$TBDLxnB{Ny!j*ck{s@d9 zE6>X`QficDrLbB>fFRI&^I>QdE2#dwK6EYeK;%3DQvZ8XipY~H29&~6QtLHoA7!+nqwYJ`TaNL% z(@aVZCBDVki?v3PDqA-Ms&f~vIYU={9XfS<$3^>}&E`uLx$Xf`hsoHnsB}`=q{``o z^zMM*?W_iEdDFqq7qta$zH2P|pc8ve&#hm{-Ibf3ltZ*~+T%Q)H?|IXQK?-K#_lWx z!Pl(_6~-2gZQ1qdD$JZx>F1;KHBbkE zTlZc;kAMUH@+_4tEN6$q?!y$5rjV55#OrH${uB$ONaOFUSB`Evb$ zNR@AJhaPYU|MaxA(}?4fO}Po;e&W@lk&LW$hT-x#Q_JX#_qbAj5(Bt?c@5Ax-QJJj z*pgp1NMy1!;A@cL3h>q3qXIfZtmoa8j>=q87A%Hp6NPX1(jaC-c;aF_?2sq?*Hges zApM&#G91Km+HF`cqqCB^#xjwHD(h{oJk}GB1knyH(Gg=~>3AD(2gj$tPx}K;sXHzr zx~vM_!GAp~pcFtdzxSwAsU!}l@q#TzMEjnWFvhmx%x9uFUk5UsS8<6WWMGMh5_ThL z`PimAZEs#z1%_z$Q#7gYY$elMyiaQ%Wi^mawDNve*k~MD98~CoC#6dIV8GK(*Nbf{ zePhLIkxVY-FxoIpq@oovqR&Oe#;^N0s?gZrG`oOWk4EtyFKP2HIGM^bk3Xw)15BS< z>d|IfikAoC``ZWbE%YVX z)9w5;DM^J)(IsITHL$y<)TfEb<=TP=Q3B&^6j&*0G*fIPRv-W6-Sj26RJp;V>7$p= zE!|Zn*=*d-+m2rzj%E3R$&ki6xR(RV`8YV>(Wy&$rTzh=zNIT-nG+6!+pg?TWfc2K z@Nn`z;V0~AqtH)Js~K>-Miul}2om;L|QV)GNXTe|bND?)s90FC!3vYJ)WG$YHHw&Q&MKO0_IxqW$ewHW> zdcE0Ic=YE7-5kTTCW?}kp3Ykak0w|r%M~2noMa^6E^$GB%#S)Y`^;-^UHU8qerkEj z#WY`04kn6RE1EN5u>#w!^1E2LA6rvLC4Q~1#hlMFWxH&`Q z;^_Qg8x|O<;PzC_f+IqzN{%tr=x;Y;jaBkJG@zDWw>Sd^W zZMs!Ae&uuZ?ba3Ek-W#4PfN?a&uTv6l(ki*TVRzT$(M-KYHv+~kbwu$QsvDq2Xplc zq)#6v4u~R-S`{W$Rn|ZwGt|7XKB!7tW3}CeyI>q)j62hWv%7MZEFEhS&;yZgZ)TrL zt?u_HeL3zfOqy(#zgW^&p2aDh>J=iV489#BAyD_jSgh2U`nhlm7Tpcws2z^ku~FMK zgIwij?P_>WoJg#W)b`0VrC70UXW>4ozDV2waunV?%6es_(aS`>_KWsFF9f;W%-|~b zXuu;_C{v=eQaP1F<7;4|tm*D-YQqVrPbqYyfdkTR2M~l(eF(?5$Law#a}n2EYZ&(pLUeEn|!H}rpa`OaX7Fmrp=rI=^XL_PEoZPiD++~LCGqpY6*^G z;YomN`TcJKV(xkhWmA!*Q+k^(OkrO^lc+&1=Z}1q59SW{I5U;3!_si|Yk~*+f!RP2 zEcUE4O98i^ECTM!dV@P#D0ya=2ehHKge@qWbbaus_*_S&*d?KDHm+GJd!@k0AUr4a zY=HWeoZ*mxJvOkkqZXd3z;?I3qcVq6EHHBjSmqlaYxCIKRY@Rh+2w6WsV+yxR_(+0 z@g#=h53ROL9u<{PCRAstp2zFw%yc-1nnlj}27}o9@t_NN<1wscZd0V=dJQR}rGTY+ zEeOT}FHe(P)B$;qD7-ZfyIIlH3(XaBWd3b^L5%xROZtfCywheN39BqHLYYJDu;$({ zAt>-VMYmn!SVT~hA05hq^}sKp7;~XP%x%|$8pZ52&kf(T^{d%h3#jsnKk9f3XGWuI z>s|+&2Kz@VbzcQ$Q)o-42HSF#utg?GNMpR{3Wgt6YbUDQqvXNPmN~A_aDL^w0dMpH zAY_5v7l{Sk=)wLscjS{JifhA%l1hH7Q`MyC@E$5@M3Dq%R7_316`KJztUod%EhyA{ zqm8WJ<9qqv`mKhaG|wCigAZpT$A(;%nd!C;jVaQH?tJQkZRPof9j^c< zg3pI*sLps?T~?!PuQmelva*E`{Z&2rNq7jklcax=0cG#dW4lz@_+%5#f3RI;^urdp zHcHg^+m(Xal!~XO{0=Jcm|cKQZ3SL^1L){?yEx0eoXJrY|$_E-&2h_U=@fUuF05m$&mP9=`BOI0jy1uQh z?580Kf=iuvM2Pv#qu*&(LO>cHAm-mnN`u!+@K@Az589_@gk;83u2QglFh(gf5Rp@v zbH08p-oIqj=fM=feos0=<`vyaM(ugKPj~l4(Wvo{4YBXr60a|C19i`00Ux3WXcnSb zD410>j{f&fu()O=4?roU=;zNaKokmF0x1yBrwH&5NHXU%HGTQ;;e&E@U%!$5yS!fj z?w3fZ;ww(miu4xz`iR@VH^$c#lmUbaegYyHjL8f}QUjn)8C)TCHw-@wW-8zbV(8fx zwG9k2Ru;=XrCXYHqtE7__=7nznvZid-J3u&I?zWuwa)NwA8I*n4 z*xBiH{mlvw-%?$w4Mz`9WfG}1LX9$AJ_6cS%s@nK(Q{A_7f}O{*=hq)8g+rV5{vzS z{%V>JQlLu=4-VvCki)Up#UM!y;W5FuaYUQzwdoVUz$rN4|7x@|Qq~>voQ9mVvbAU; ze2`2E_LS45m69=RaELM z9E>J}69?<6WeLltmsR{578-zyelDogxlTifCPc#AxB<-Hu3*88L;%oFa}Yh@#fBw5 z`7OX8eS5)0fpuwYQDUo9j{Nh^U++)8(KdVK1Y#n8=t%xIq)|=*3Gf*I#RR_n#DxGr zp${M-y#K!CO8~I5>nwD@JpDH-^@dBC0{|c;(?N*rAE;5{jh~W`M-2EM)ClbHV*yHn z*H@PoA@~DmABY*{fH*op0|sIP*dlBzn?cCGUm}8=`9>U#*N#_xMh9I{up5vkg60F| z4}G^>fTxf_`5*eQhX^34HmjCY066sThn*WV_4*JX?B={uXbt{HQvv@)dk5SN>^pr! z-hb}-T!6=JJ`d21nq?dQgIWTO#2*J#l)qY2h5Vn((Y^vqD#q8Vk>vljO&1sfemZvC z-(&1=UMqF8q^%7*^;#t_21qFtViPU+LyZ32_MU)@)Fpq{+eYB;mj|9Tpxm(q#72Q& zgXct&KH7hXQlLFPW&l<3)BJ$+mz%NrB3M0C;xF2akEFtEo?2u_1%L*(K+1CFviwt+ z{?jMkgtU91)gvPkBN^;TwY&gQ#q$XP{BnJLGWDg!<&xHD4D;}vJ3lBL&w7W?dpg{F z;hs0yIu^H^l3iYIbt;V}q;ZZ*3GO_O2GJJ)O$;=zcaG+vL|kZgvZ4%=UgRe{1N@s+ zt57lMe!@6Do!H}oIPhZnz^*g2+8<1M?kV<5vHyIf1VDk2B=+LaR}C{J2!*5h018(; zs`(WW==2K1zR?}gMB3`mkrs2&rN0{}l8;V5<8X1D3}MlHV3LZiSPe~5#d+AtW@M?SzwZpp&XWe9II2uF=yFE&bAYcytsVR#g@K)^&dG*bn$~u~nd~KxHd+ z<;$GgcQi^6*Aeh4HT~kVfG1iR6uY`yTTg&AeQx4yFWT{Dz;fhMU?5>Wf$Y;0e$MUn ze(-*Ug#uxHH~TTC%5v`~E9q>91c3;TnI3*JVO%IZsA?Kf?^2D4T)e2TjR@{n&?$vW zz~PYpWk2in$;dbcoy`yejU;^0vR9&3<{LDqTD@iOSO$B}J{hI&xlK9(0FtKkJpw`@ zIhzuGopvJ#3oR}TKZ}d?6kl>E8_e|pUA7@cOdJE?IxJm_jOx3hlz}8$z+6QiqkKjP zHUNFYS~e*Yhj|zM2;PL?fNuDN%r8g>vad*uY{Vhb5#9m7qN$MGM*lu&|KC6UBeMnv z0{WFI@|^(jpJDOe&sqXQ=1riD-`m^9A|3A=CW_C0#=sv#@!j_gfRcel^8Nq$=U;|o z>5Y4~VB~Z8-yr{U@!#+KKSgY&b-X+SiMR;?z+}eU##?dq-+IZs38NML`~=1SXDo}Z zpL)Hr1Fg!+0kG=lkly+Kz?FZKM`CZtqe1OAQUr*ocTTe~Q5bpvk=cU^VzN3nG4al_ z1VQV1><^XwX4F`Ku?ijZWh6EJ!H656=PujMTlP;aExFm*G`+WnZ* z8Q>0H--_b?q+%%aw;B5TC)xv02*!iM2*MxBI0A<@5aOMfh{ExFH9asg(gS2Ohhtc^ z!1wm{)(C~ASV4dI8oQX7fI|&9dbRf!kd9Q%6?D;%;rO4G zLlb#3afBAX|47~uYKv*(YA{NEICBv(`-+464~X?w=`2J*>FJ6bVUqu};iSmhXZ@(S zODBDadaeKcQeZa>K`7uB1L#imC1#xJHo(=e?|)f|V?+#5|7u!3z23r}VCYjf;TU$%qq6Y0T4a8Z2#IpB>v?*lw6#U~%m2--(K%dF{X~>@LLE zc(BEKh-Pw9>o1b&qn8bxWMq|UUYw@)j$$eX*a|3V z1YneJ743nyU3`M$`J4XA+%okl_P9gZrXJ-{`{uz7#MYx5ACi=G*AjQn%R;R}*JE{7 z*91@dn+@RT zwiT9+_LzdRiSu|bpq(4o({8>F8+*T)&xXv(b=Tbb*T1A{`BE6I1glv#qBBebf)>u3 z=b>N^JUk;upN4EE1yRZP%=3T#B-ehqCURsHglOTKQtc2g`;~+f>^rttaF}ZocSt{%ek20y5FUh-#m{^4uO zO-*dAnq2qWad#i+dcoVu<>OLO4N* z_SWx&xrgU6ek(b35#Ld{7pjW{r2;rrjJ2}*<<1w2YUKHpnN>i9ZS`f?9@7ApSf^`b zm{y3o?6zEsD%a6N-Abv3uJxg8H(1u(xOjBPgvOfum)CWT{QImdz=mEw0!-Fk2gp4W zHKagm#&pHTR?jIfyXo2H$<}~!{7+8W^l_-t+v&i^7_{GQ7{rFyHQh03LFWWD?Bn;q znUx)=R(-RX*?VlN0Z7Zb(ztHaVr^7i5+a=>IG93`_pT*&Ou@HOl*!J_rpt8%1dCYe z`emXg0~B2==iQUDJDy3TUl0FQ;wzHDNzl9TwXeZckElGs<%r=S1rs<#7PAg~GnBx# z1J2aV@o0e#5Ncw~yKq121jh^bOmxPhn(KhI}cbqC$$>!f#+xe$I+yZKVvjy1o+UNsLbtk(RRi7%*rge(Y<@(ztpqNWFU2?_n_F z>H##Y*kgO!4VFzSEk?|1trS^kaG;l&^-^=RiVSA}39+%c(8? z(BHENrJ$0|%o)az!tio2q^>f8Bv^%4&2@T1g6*SNebgr{53K|m+wXRuHGIFkoU%Qz zZ#Sz9^pB~G0>0yhf{It^;dzFb)yey3HW{mt2aLi6cvA`+7$Mt96rKBhE*pviRI@3C&p3z_~stYxT4kNm|s>U zK}JO-Lw;C7g;--{uGaktzD}MDejmM*w&u2VcNc1LQI

!8SpWd^C_a_gSRWS@V0m z;!MEASmP9jO`a4P1(~dEHCAXh9`~Y{rc6jx?o05zM5!96jAe>f3Vyp&s|R7$=(vT< zF!fFuBUCB!FY6QY#GUBS6fq!-m#ylCbKYo}TOj71apQL+WxaUnTC238ym` zPXGc5M5ak^3Af2>d0S+}zX$(xmq3RZRvjois>`ojmPNay&=70&<63!hSHbmx&b56& z)AU!NY;HY;!6#IH;gJ4Ki72z#!ULqacPGw5(mq`Mmoc;RQzaw)wmI`pPo^E$*^4Uc z_mXuW*Ye55gYdN&P%~f?cd07o$>v5?NdAKlFKl8g{3f^=E9EkvMN*HEcw)FPz1F4&Q5~t5LRd&@Nc4C05mdu;dOh3a>4SU- zV#URz>|_}w>i%CUY=T*%6KsJP(s_<^R4MiODX_?}R=5wof3QRo+ zCuuEgwC%a#UHkEyt{d`;dv%`4)P~&47We*0_%vi3EKdl$et2t0r7z+Yr*C+vSqK-==6ezI>yxf2QaCVZp0H$-Cwy*T~32j z>lb!~W~NP__R8K7pGXiRguw5~xO?S|W`DKIZWsw!T2f`95Q{FZtH(F&7TO3s=@?U# zzL%eVT2bGIwDX!LQ9Af^&EEuKd}?b~ZDZ8;%3HeE@l*IMP2=lguMd_XObRm2x{E21#svF%`}(34TuWlJH-VSV z<^aX+bkfuQ`q;h&946=rmSD&uU2n!}5iHt!(c0!gKz9EZ-xOFteAKX}9}O?IATLgWZ7~lpstm#VlbUHC&rNCG2y|Du8b`@;98|a7>Z%Ea_-0egV^YrXWvUVlTh*Fr}<9#INq7Qm%qNE$3J9*{f zZICt@^!1VCVBjm|lOq$9nLl!mmFc&df$Zl~;|{AC^bYe9WzYAf=_l|%%L$KhawW~1 z@#+(p*<*Xci=grjX+3wDwCq;uE9M>g5#)BaVrC7=nlc?y$a=;cb87ctx0h{K;4K;+ zh6m?D&VYbchc`TsRx;1!QrcVB`dP2GjwkxjV#(K>t|9hND45CE2oZ%!{5E8_iStS< zrRleCvS#nq0~mqiHP4@4re$*vn=+3$F x>&?1>D6Q@2OUflElyEheb8bz4Dgm~*z3TyBG` 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

+`9CPEi&(EF^@aX(72EV9&N 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 C8DqHs diff --git a/docs/assets/images/warehouse.png b/docs/assets/images/warehouse.png deleted file mode 100755 index f6a0262d211c731d4184b62ba5395acaadbb9c7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20919 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 - Find out more - \ No newline at end of file diff --git a/docs/macros.md b/docs/macros.md deleted file mode 100644 index f3aff01a6..000000000 --- a/docs/macros.md +++ /dev/null @@ -1,1277 +0,0 @@ -## Table templates -######(macros/tables) - -These macros form the core of the package and can be called in your models to build the different types of tables needed -for your Data Vault. - -### hub - -Generates sql to build a hub table using the provided metadata in the ```dbt_project.yml```. - -```jinja2 -{{ dbtvault.hub(var('src_pk'), var('src_nk'), var('src_ldts'), - var('src_source'), var('source')) }} -``` - -#### Parameters - -| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | -| ------------- | --------------------------------------------------- | -------------------- | --------------- | -------------------------------------------------------------------------------------- | -| src_pk | Source primary key column | String | String | check_circle | | -| src_nk | Source natural key column | String | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| source | Staging model reference or table name | String | List (YAML) | check_circle | - -#### Usage - -``` jinja2 - -{{- config(...) -}} - -{{ dbtvault.hub(var('src_pk'), var('src_nk'), var('src_ldts'), - var('src_source'), var('source')) }} -``` - -#### Example Output - -```mysql tab='Single-Source' -SELECT DISTINCT - stg.CUSTOMER_PK, - stg.CUSTOMER_KEY, - stg.LOADDATE, - stg.SOURCE -FROM ( - SELECT a.CUSTOMER_PK, a.CUSTOMER_KEY, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.v_stg_orders AS a -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.hub_customer AS tgt -ON stg.CUSTOMER_PK = tgt.CUSTOMER_PK -WHERE tgt.CUSTOMER_PK IS NULL -``` - -```mysql tab='Union' -SELECT DISTINCT - stg.PART_PK, - stg.PART_KEY, - stg.LOADDATE, - stg.SOURCE -FROM ( - SELECT src.PART_PK, src.PART_KEY, src.LOADDATE, src.SOURCE, - LAG(SOURCE, 1) - OVER(PARTITION by PART_PK - ORDER BY PART_PK) AS FIRST_SOURCE - FROM ( - SELECT a.PART_PK, a.PART_KEY, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.v_stg_orders AS a - UNION - SELECT b.PART_PK, b.PART_KEY, b.LOADDATE, b.SOURCE - FROM MYDATABASE.MYSCHEMA.v_stg_inventory AS b - ) AS src -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.hub_part AS tgt -ON stg.PART_PK = tgt.PART_PK -WHERE tgt.PART_PK IS NULL -AND stg.FIRST_SOURCE IS NULL - -``` -___ - -### link - -Generates sql to build a link table using the provided metadata in the ```dbt_project.yml```. - -```jinja2 -{{ dbtvault.link(var('src_pk'), var('src_fk'), var('src_ldts'), - var('src_source'), var('source')) }} -``` - -#### Parameters - -| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | -| ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | check_circle | -| src_fk | Source foreign key column(s) | List (YAML) | List (YAML) | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| source | Staging model reference or table name | String | List (YAML) | check_circle | - -#### Usage - -``` jinja2 - -{{- config(...) -}} - -{{ dbtvault.link(var('src_pk'), var('src_fk'), var('src_ldts'), - var('src_source'), var('source')) }} -``` - -#### Example Output - -```mysql tab='Single-Source' -SELECT DISTINCT - stg.LINK_CUSTOMER_NATION_PK, - stg.CUSTOMER_PK, - stg.NATION_PK, - stg.LOADDATE, - stg.SOURCE -FROM ( - SELECT a.LINK_CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.v_stg_orders AS a -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation AS tgt -ON stg.LINK_CUSTOMER_NATION_PK = tgt.LINK_CUSTOMER_NATION_PK -WHERE tgt.LINK_CUSTOMER_NATION_PK IS NULL -``` - -```mysql tab='Union' -SELECT DISTINCT - stg.NATION_REGION_PK, - stg.NATION_PK, - stg.REGION_PK, - stg.LOADDATE, - stg.SOURCE -FROM ( - SELECT src.NATION_REGION_PK, src.NATION_PK, src.REGION_PK, src.LOADDATE, src.SOURCE, - LAG(SOURCE, 1) - OVER(PARTITION by NATION_REGION_PK - ORDER BY NATION_REGION_PK) AS FIRST_SOURCE - FROM ( - SELECT a.NATION_REGION_PK, a.NATION_PK, a.REGION_PK, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.v_stg_orders AS a - UNION - SELECT b.NATION_REGION_PK, b.NATION_PK, b.REGION_PK, b.LOADDATE, b.SOURCE - FROM MYDATABASE.MYSCHEMA.v_stg_inventory AS b - ) AS src -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.link_nation_region AS tgt -ON stg.NATION_REGION_PK = tgt.NATION_REGION_PK -WHERE tgt.NATION_REGION_PK IS NULL -AND stg.FIRST_SOURCE IS NULL -``` - -___ - -### sat - -Generates sql to build a satellite table using the provided metadata in the ```dbt_project.yml```. - -```jinja2 -{{ dbtvault.sat(var('src_pk'), var('src_hashdiff'), var('src_payload'), - var('src_eff'), var('src_ldts'), var('src_source'), - var('source')) }} -``` - -#### Parameters - -| Parameter | Description | Type | Required? | -| ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_hashdiff | Source hashdiff column | String | check_circle | -| src_payload | Source payload column(s) | List (YAML) | check_circle | -| src_eff | Source effective from column | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| source | Staging model reference or table name | List (YAML) | check_circle | - -#### Usage - - -``` jinja2 - -{{- config(...) -}} - -{{ dbtvault.sat(var('src_pk'), var('src_hashdiff'), var('src_payload'), - var('src_eff'), var('src_ldts'), var('src_source'), - var('source')) }} -``` - - -#### Example Output - -```mysql -SELECT DISTINCT - e.CUSTOMER_PK, - e.CUSTOMER_HASHDIFF, - e.NAME, - e.ADDRESS, - e.PHONE, - e.ACCBAL, - e.MKTSEGMENT, - e.COMMENT, - e.EFFECTIVE_FROM, - e.LOADDATE, - e.SOURCE -FROM MYDATABASE.MYSCHEMA.v_stg_orders AS e -LEFT JOIN ( - SELECT d.CUSTOMER_PK, d.CUSTOMER_HASHDIFF, d.NAME, d.ADDRESS, d.PHONE, d.ACCBAL, d.MKTSEGMENT, d.COMMENT, d.EFFECTIVE_FROM, d.LOADDATE, d.SOURCE - FROM ( - SELECT c.CUSTOMER_PK, c.CUSTOMER_HASHDIFF, c.NAME, c.ADDRESS, c.PHONE, c.ACCBAL, c.MKTSEGMENT, c.COMMENT, c.EFFECTIVE_FROM, c.LOADDATE, c.SOURCE, - CASE WHEN RANK() - OVER (PARTITION BY c.CUSTOMER_PK - ORDER BY c.LOADDATE DESC) = 1 - THEN 'Y' ELSE 'N' END CURR_FLG - FROM ( - SELECT a.CUSTOMER_PK, a.CUSTOMER_HASHDIFF, a.NAME, a.ADDRESS, a.PHONE, a.ACCBAL, a.MKTSEGMENT, a.COMMENT, a.EFFECTIVE_FROM, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.sat_order_customer_details as a - JOIN MYDATABASE.MYSCHEMA.v_stg_orders as b - ON a.CUSTOMER_PK = b.CUSTOMER_PK - ) as c - ) AS d -WHERE d.CURR_FLG = 'Y') AS src -ON src.CUSTOMER_HASHDIFF = e.CUSTOMER_HASHDIFF -WHERE src.CUSTOMER_HASHDIFF IS NULL -``` -___ - -### t_link - -Generates sql to build a transactional link table using the provided metadata in the dbt_project.yml. - -```jinja2 -{{ dbtvault.t_link(var('src_pk'), var('src_fk'), var('src_payload'), - var('src_eff'), var('src_ldts'), var('src_source'), - var('source')) }} -``` - -#### Parameters - -| Parameter | Description | Type | Required? | -| ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_fk | Source foreign key column(s) | List (YAML) | check_circle | -| src_payload | Source payload column(s) | List (YAML) | check_circle | -| src_eff | Source effective from column | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| source | Staging model reference or table name | List (YAML) | check_circle | - -#### Usage - - -``` jinja2 - -{{- config(...) -}} - -{{ dbtvault.t_link(var('src_pk'), var('src_fk'), var('src_payload'), - var('src_eff'), var('src_ldts'), var('src_source'), - var('source')) }} -``` - -#### Example Output - -```mysql -SELECT DISTINCT - stg.TRANSACTION_PK, - stg.CUSTOMER_FK, - stg.ORDER_FK, - stg.TRANSACTION_NUMBER, - stg.TRANSACTION_DATE, - stg.TYPE, - stg.AMOUNT, - stg.EFFECTIVE_FROM, - stg.LOADDATE, - stg.SOURCE -FROM ( - SELECT stg.TRANSACTION_PK, stg.CUSTOMER_FK, stg.ORDER_FK, stg.TRANSACTION_NUMBER, stg.TRANSACTION_DATE, stg.TYPE, stg.AMOUNT, stg.EFFECTIVE_FROM, stg.LOADDATE, stg.SOURCE - FROM MYDATABASE.MYSCHEMA.v_stg_transactions AS stg -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.t_link_transactions AS tgt -ON stg.TRANSACTION_PK = tgt.TRANSACTION_PK -WHERE tgt.TRANSACTION_PK IS NULL -``` -___ - -### eff_sat - -!!! tip "Cutting edge release" - **This feature is currently unreleased. Whilst it has been fully tested, we recommend that you use it with care.** - - If you find any bugs or would like to recommend improvements or additions, please - [submit an issue](https://github.com/Datavault-UK/dbtvault/issues). - -Generates sql to build a effectivity satellite table using the provided metadata in the dbt_project.yml. - -```jinja2 -{{ dbtvault.eff_sat(var('src_pk'), var('src_dfk'), var('src_sfk'), var('src_ldts'), - var('src_eff_from'), var('src_start_date'), var('src_end_date'), - var('src_source'), var('link'), var('source')) }} -``` - -#### Parameters - -| Parameter | Description | Type (Single-part keys) | Type (Multi-part keys) | Required? | -| -------------- | -------------------------------------------------------- | ----------------------- | ----------------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | check_circle | -| src_dfk | Source driving foreign key column | String | String/List (YAML) | check_circle | -| src_sfk | Source secondary foreign key column | String | String/List (YAML) | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_eff_from | Source effective from column | String | String | check_circle | -| src_start_date | The date which a link record is open/closed from | String | String | check_circle | -| src_end_date | The date which a link record is open/closed to | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| link | The link which this effectivity satellite is attached to | String | String | check_circle | -| source | Staging model reference or table name | String | String | check_circle | | | check_circle | - - -#### Usage - -``` jinja2 -{{- config(...) -}} --- depends_on: {{ ref(var('link')) }} -{{ dbtvault.eff_sat(var('src_pk'), var('src_dfk'), var('src_sfk'), var('src_ldts'), - var('src_eff_from'), var('src_start_date'), var('src_end_date'), - var('src_source'), var('link'), var('source')) }} -``` - -!!! note - Currently, we have the extra line of code - ```-- depends_on: {{ ref(var('link')) }}```. This is due to the structure of dependencies in dbt. An alternative method is - being investigated but this fix currently passes all the our tests. - -#### Example output - -Here are some example outputs for the incremental steps of effectivity satellite models. - -```mysql tab='Single-part key' -WITH -c AS ( - SELECT DISTINCT - a.CUSTOMER_ORDER_PK, a.LOADDATE, a.EFFECTIVE_FROM, a.START_DATETIME, a.END_DATETIME, a.SOURCE - FROM DBT_VAULT.TEST_vlt.test_eff_customer_order_current AS a - INNER JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS b ON a.CUSTOMER_ORDER_PK=b.CUSTOMER_ORDER_PK - ) -, d as ( - SELECT - c.CUSTOMER_ORDER_PK, c.LOADDATE, c.EFFECTIVE_FROM, c.START_DATETIME, c.END_DATETIME, c.SOURCE, - CASE WHEN RANK() - OVER (PARTITION BY c.CUSTOMER_ORDER_PK - ORDER BY c.END_DATETIME ASC) = 1 - THEN 'Y' ELSE 'N' END AS CURR_FLG - FROM c - ) -, p AS ( - SELECT q.* FROM DBT_VAULT.TEST_vlt.test_link_customer_order_current AS q - INNER JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS r ON q.CUSTOMER_FK=r.CUSTOMER_FK - ) -, x AS ( - SELECT p.* - , s.CUSTOMER_FK AS DFK_1 - FROM p - LEFT JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS s ON p.CUSTOMER_FK=s.CUSTOMER_FK - AND p.ORDER_FK=s.ORDER_FK - WHERE (s.CUSTOMER_FK IS NULL AND s.ORDER_FK IS NULL) - ) -, y AS ( - SELECT - t.CUSTOMER_ORDER_PK, t.LOADDATE, t.SOURCE, t.EFFECTIVE_FROM, t.START_DATETIME, t.END_DATETIME - , x.DFK_1 - , x.CUSTOMER_FK, - CASE WHEN RANK() - OVER (PARTITION BY t.CUSTOMER_ORDER_PK - ORDER BY t.END_DATETIME ASC) = 1 - THEN 'Y' ELSE 'N' END AS CURR_FLG - FROM x - INNER JOIN DBT_VAULT.TEST_vlt.test_eff_customer_order_current AS t ON x.CUSTOMER_ORDER_PK=t.CUSTOMER_ORDER_PK - ) - -SELECT DISTINCT - e.CUSTOMER_ORDER_PK, e.LOADDATE, e.SOURCE, e.EFFECTIVE_FROM, - e.EFFECTIVE_FROM AS START_DATETIME, - e.END_DATETIME -FROM DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS e -LEFT JOIN ( - SELECT d.CUSTOMER_ORDER_PK, d.LOADDATE, d.EFFECTIVE_FROM, d.START_DATETIME, d.END_DATETIME, d.SOURCE - FROM d - WHERE d.CURR_FLG = 'Y' AND d.END_DATETIME=TO_DATE('9999-12-31') - ) AS eff -ON eff.CUSTOMER_ORDER_PK=e.CUSTOMER_ORDER_PK -WHERE (eff.CUSTOMER_ORDER_PK IS NULL -AND e.ORDER_FK<>MD5_BINARY('^^') AND e.CUSTOMER_FK<>MD5_BINARY('^^')) -UNION -SELECT - y.CUSTOMER_ORDER_PK, - z.LOADDATE, - y.SOURCE, y.EFFECTIVE_FROM, y.START_DATETIME, - CASE WHEN - y.DFK_1 IS NULL - THEN z.EFFECTIVE_FROM ELSE '9999-12-31' END AS END_DATETIME -FROM y -LEFT JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS z ON y.CUSTOMER_FK=z.CUSTOMER_FK -WHERE (y.CURR_FLG='Y' AND y.END_DATETIME='9999-12-31') -``` - -```mysql tab='Multi-part key' -WITH -c AS ( - SELECT DISTINCT - a.CUSTOMER_ORDER_PK, a.LOADDATE, a.EFFECTIVE_FROM, a.START_DATETIME, a.END_DATETIME, a.SOURCE - FROM DBT_VAULT.TEST_vlt.test_eff_customer_order_multipart_current AS a - INNER JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS b ON a.CUSTOMER_ORDER_PK=b.CUSTOMER_ORDER_PK - ) -, d as ( - SELECT - c.CUSTOMER_ORDER_PK, c.LOADDATE, c.EFFECTIVE_FROM, c.START_DATETIME, c.END_DATETIME, c.SOURCE, - CASE WHEN RANK() - OVER (PARTITION BY c.CUSTOMER_ORDER_PK - ORDER BY c.END_DATETIME ASC) = 1 - THEN 'Y' ELSE 'N' END AS CURR_FLG - FROM c - ) -, p AS ( - SELECT q.* FROM DBT_VAULT.TEST_vlt.test_link_customer_order_multipart_current AS q - INNER JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS r ON q.CUSTOMER_FK=r.CUSTOMER_FK - AND q.NATION_FK=r.NATION_FK - ) -, x AS ( - SELECT p.* - , s.CUSTOMER_FK AS DFK_1 - , s.NATION_FK AS DFK_2 - FROM p - LEFT JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS s ON p.CUSTOMER_FK=s.CUSTOMER_FK AND p.NATION_FK=s.NATION_FK - AND p.ORDER_FK=s.ORDER_FK AND p.PRODUCT_FK=s.PRODUCT_FK AND p.ORGANISATION_FK=s.ORGANISATION_FK - WHERE (s.CUSTOMER_FK IS NULL AND s.NATION_FK IS NULL - AND s.ORDER_FK IS NULL AND s.PRODUCT_FK IS NULL AND s.ORGANISATION_FK IS NULL) - ) -, y AS ( - SELECT - t.CUSTOMER_ORDER_PK, t.LOADDATE, t.SOURCE, t.EFFECTIVE_FROM, t.START_DATETIME, t.END_DATETIME - , x.DFK_1 - , x.DFK_2 - , x.CUSTOMER_FK - , x.NATION_FK, - CASE WHEN RANK() - OVER (PARTITION BY t.CUSTOMER_ORDER_PK - ORDER BY t.END_DATETIME ASC) = 1 - THEN 'Y' ELSE 'N' END AS CURR_FLG - FROM x - INNER JOIN DBT_VAULT.TEST_vlt.test_eff_customer_order_multipart_current AS t ON x.CUSTOMER_ORDER_PK=t.CUSTOMER_ORDER_PK - ) - -SELECT DISTINCT - e.CUSTOMER_ORDER_PK, e.LOADDATE, e.SOURCE, e.EFFECTIVE_FROM, - e.EFFECTIVE_FROM AS START_DATETIME, - e.END_DATETIME -FROM DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS e -LEFT JOIN ( - SELECT d.CUSTOMER_ORDER_PK, d.LOADDATE, d.EFFECTIVE_FROM, d.START_DATETIME, d.END_DATETIME, d.SOURCE - FROM d - WHERE d.CURR_FLG = 'Y' AND d.END_DATETIME=TO_DATE('9999-12-31') - ) AS eff -ON eff.CUSTOMER_ORDER_PK=e.CUSTOMER_ORDER_PK -WHERE (eff.CUSTOMER_ORDER_PK IS NULL AND e.ORDER_FK<>MD5_BINARY('^^') AND e.PRODUCT_FK<>MD5_BINARY('^^') - AND e.ORGANISATION_FK<>MD5_BINARY('^^') AND e.CUSTOMER_FK<>MD5_BINARY('^^') AND e.NATION_FK<>MD5_BINARY('^^')) -UNION -SELECT - y.CUSTOMER_ORDER_PK, - z.LOADDATE, - y.SOURCE, y.EFFECTIVE_FROM, y.START_DATETIME, - CASE WHEN - y.DFK_1 IS NULL - AND y.DFK_2 IS NULL - THEN z.EFFECTIVE_FROM ELSE '9999-12-31' END AS END_DATETIME -FROM y -LEFT JOIN DBT_VAULT.TEST_stg.test_stg_eff_sat_hashed_current AS z ON y.CUSTOMER_FK=z.CUSTOMER_FK AND y.NATION_FK=z.NATION_FK -WHERE (y.CURR_FLG='Y' AND y.END_DATETIME='9999-12-31') -``` - -___ - -## Staging Macros -######(macros/staging) - -These macros are intended for use in the staging layer. -___ - -### multi_hash - -!!! warning - This macro ***should not be*** used for cryptographic purposes. - - The intended use is for creating checksum-like values only, so that we may compare records accurately. - - [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) - -!!! seealso "See Also" - - [hash](#hash) - - [Hashing best practises and why we hash](bestpractices.md#hashing) - - With the release of dbtvault 0.4, you may now choose between ```MD5``` and ```SHA-256``` hashing. - [Learn how](bestpractices.md#choosing-a-hashing-algorithm-in-dbtvault) - -This macro will generate SQL hashing sequences for one or more columns as below: - -```sql tab='MD5' -CAST(MD5_BINARY(IFNULL((UPPER(TRIM(CAST(column1 AS VARCHAR)))), '^^')) AS BINARY(16)) AS alias1, -CAST(MD5_BINARY(IFNULL((UPPER(TRIM(CAST(column1 AS VARCHAR)))), '^^')) AS BINARY(16)) AS alias2 -``` - -```sql tab='SHA' -CAST(SHA2_BINARY(IFNULL((UPPER(TRIM(CAST(column1 AS VARCHAR)))), '^^')) AS BINARY(32)) AS alias1, -CAST(SHA2_BINARY(IFNULL((UPPER(TRIM(CAST(column2 AS VARCHAR)))), '^^')) AS BINARY(32)) AS alias2 -``` - -#### Parameters - -| Parameter | Description | Type | Required? | -| ---------------- | ---------------------------------------------- | -------- | -------------------------------------------------------- | -| pairs | (column, alias) pair | Tuple | check_circle | -| pairs: columns | Single column string or list of columns | String | check_circle | -| pairs: alias | The alias for the column | String | check_circle | -| pairs: sort | Will alpha sort columns if true, default false. | Boolean | clear | - - -#### Usage - -```yaml -{{ dbtvault.multi_hash([('CUSTOMERKEY', 'CUSTOMER_PK'), - (['CUSTOMERKEY', 'NAME', 'PHONE', 'DOB'], - 'HASHDIFF', true)]) }} -``` - -#### Output - -```mysql tab='MD5' -CAST(MD5_BINARY(IFNULL((UPPER(TRIM(CAST(column1 AS VARCHAR)))), '^^')) AS BINARY(16)) AS CUSTOMER_PK, - -CAST(MD5_BINARY(CONCAT( - IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) AS BINARY(16)) AS HASHDIFF -``` - -```mysql tab='SHA' -CAST(SHA2_BINARY(IFNULL((UPPER(TRIM(CAST(column1 AS VARCHAR)))), '^^')) AS BINARY(32)) AS CUSTOMER_PK, - -CAST(SHA2_BINARY(CONCAT( - IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) AS BINARY(32)) AS HASHDIFF -``` - -!!! success "Column sorting" - If you wish to sort columns in alphabetical order as per [best practices](bestpractices.md#hashing), - you do not need to worry about doing this manually, just set the - ```sort``` flag to true when creating hashdiffs as per the above example. -___ - -### add_columns - -!!! note - As of v0.5, column aliasing must be implemented using this macro. Manual type mappings in the raw vault are now - deprecated due to bad practice. - -A simple macro for generating sequences of the following SQL: -```mysql -column AS alias -``` - -#### Parameters - -| Parameter | Description | Type | Required? | -| ------------- | ----------------------------------- | -------------- | ----------------------------------------------- | -| source_table | A source reference | Source | clear | -| pairs | List of (column, alias) pairs | List of tuples | clear | - -!!! note - At least one of the above parameters must be provided, both may be provided if required. - -#### Usage - -```yaml -{{ dbtvault.add_columns(source('MYSOURCE', 'MYTABLE'), - [('CURRENT_DATE()', 'EFFECTIVE_FROM'), - ('!STG_CUSTOMER', 'SOURCE'), - ('OLD_CUSTOMER_PK', 'CUSTOMER_PK']) }} -``` - -#### Output - -```mysql -, -CURRENT_DATE() AS EFFECTIVE_FROM, -'STG_CUSTOMER' AS SOURCE, -OLD_CUSTOMER_PK AS CUSTOMER_PK -``` - -#### Specific usage notes - -##### Getting columns from the source -The ```add_columns``` macro will automatically select all columns from the optional ```source_table``` reference, -if provided. - -##### Overriding source columns - -You may wish to override some of the source columns with different values. To replace the ```SOURCE``` -or ```LOADDATE``` column value, for example, then you must provide the column name -that you wish to override as the alias in the pair. - -!!! note - If a provided column name is the same as a source column name, the provided - column will take precedence over the source column, and the original source column will not be selected. - -##### Functions - -Database functions may be used, for example ```CURRENT_DATE()``` to set the current date as the value of a column, as on -```line 2``` of the usage example. Any function supported by the database is valid, for example ```LPAD()```, which pads -a column with leading zeroes. - -##### Adding constants -With the ```add_columns``` macro, you may provide constants. -These are additional 'calculated' columns created from hard-coded values. -To achieve this, simply provide the constant with a ```!``` in front of the desired constant, -and the macro will do the rest. See ```line 3``` of the usage example above, and the output it gives. - -##### Aliasing columns - -As of release 0.3, columns should now be aliased in the staging layer prior to loading. This can be achieved by providing the -column name you wish to alias as the first argument in a pair, and providing the alias for that column as the second argument. -This can be observed on ```line 4``` of the usage example above. Aliasing can still be carried out using a -manual mapping (shown in the [table template](#table-templates) section examples) but this is less concise for aliasing -purposes. - -___ - -### from - -Used in creating source/hashing models to complete a staging layer model. - -```mysql -FROM MYDATABASE.MYSCHEMA.MYTABLE -``` - -!!! info - Sources need to be set up in dbt to ensure this works. [Read More](https://docs.getdbt.com/v0.15.0/docs/using-sources) - -#### Parameters - -| Parameter | Description | Type | Required? | -| ------------- | ----------------------------------------- | ------ | -------------------------------------------------------- | -| source_table | A source reference | Source | check_circle | - -#### Usage - -```yaml -{{ dbtvault.from( source('MYSOURCE', 'MYTABLE') ) }} -``` - -#### Output - -```mysql -FROM MYDATABASE.MYSCHEMA.MYTABLE -``` - -___ - -## Supporting Macros -######(macros/supporting) - -Supporting macros are helper functions for use in models. It should not be necessary to call these macros directly, however they -are used extensively in the [table templates](#table-templates). - -___ - -### cast - -A macro for generating cast sequences: - -```mysql -CAST(prefix.column AS type) AS alias -``` - -#### Parameters - -| Parameter | Description | Required? | -| ---------------- | ----------------------------- | -------------------------------------------------------- | -| columns | Triples or strings | check_circle | -| prefix | A string | clear | - -#### Usage - -!!! note - As shown in the snippet below, columns must be provided as a list. - The collection of items in this list can be any combination of: - - - ```(column, type, alias) ``` 3-tuples - - ```[column, type, alias] ``` 3-item lists - - ```'DOB'``` Single strings. - -```yaml - -{%- set tgt_pk = ['PART_PK', 'BINARY(16)', 'PART_PK'] -%} - -{{ dbtvault.cast([tgt_pk, - 'DOB', - ('PART_PK', 'NUMBER(38,0)', 'PART_ID'), - ('LOADDATE', 'DATE', 'LOADDATE'), - ('SOURCE', 'VARCHAR(15)', 'SOURCE')], - 'stg') }} -``` - -#### Output - -```mysql -CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, -stg.DOB, -CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, -CAST(stg.LOADDATE AS DATE) AS LOADDATE, -CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE -``` - -___ - -### hash - -!!! warning - This macro ***should not be*** used for cryptographic purposes. - - The intended use is for creating checksum-like values only, so that we may compare records accurately. - - [Read More](https://www.md5online.org/blog/why-md5-is-not-safe/) - -!!! seealso "See Also" - - [multi-hash](#multi_hash) - - [Hashing best practises and why we hash](bestpractices.md#hashing) - - With the release of dbtvault 0.4, you may now choose between ```MD5``` and ```SHA-256``` hashing. - [Learn how](bestpractices.md#choosing-a-hashing-algorithm-in-dbtvault) - -A macro for generating hashing SQL for columns: - -```sql tab='MD5' -CAST(MD5_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(16)) AS alias -``` - -```sql tab='SHA' -CAST(SHA2_BINARY(UPPER(TRIM(CAST(column AS VARCHAR)))) AS BINARY(32)) AS alias -``` - -- Can provide multiple columns as a list to create a concatenated hash -- Columns are sorted alphabetically (by alias) if you set the ```sort``` flag to true. -- Generally, you should alpha sort hashdiffs using the ```sort``` flag. -- Casts a column as ```VARCHAR```, transforms to ```UPPER``` case and trims whitespace -- ```'^^'``` Accounts for null values with a double caret -- ```'||'``` Concatenates with a double pipe - -#### Parameters - -| Parameter | Description | Type | Required? | -| ---------------- | ----------------------------------------------- | ----------- | -------------------------------------------------------- | -| columns | Columns to hash on | String/List | check_circle | -| alias | The name to give the hashed column | String | check_circle | -| sort | Will alpha sort columns if true, default false. | Boolean | clear | - - -#### Usage - -```yaml -{{ dbtvault.hash('CUSTOMERKEY', 'CUSTOMER_PK') }}, -{{ dbtvault.hash(['CUSTOMERKEY', 'PHONE', 'DOB', 'NAME'], 'HASHDIFF', true) }} -``` - -!!! tip - [multi_hash](#multi_hash) may be used to simplify the hashing process and generate multiple hashes with one macro. - -#### Output - -```mysql tab='MD5' -CAST(MD5_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(16)) AS CUSTOMER_PK, -CAST(MD5_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) - AS BINARY(16)) AS HASHDIFF -``` - -```mysql tab='SHA' -CAST(SHA2_BINARY(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR)))) AS BINARY(32)) AS CUSTOMER_PK, -CAST(SHA2_BINARY(CONCAT(IFNULL(UPPER(TRIM(CAST(CUSTOMERKEY AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(DOB AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(NAME AS VARCHAR))), '^^'), '||', - IFNULL(UPPER(TRIM(CAST(PHONE AS VARCHAR))), '^^') )) - AS BINARY(32)) AS HASHDIFF -``` - -___ - -### prefix - -A macro for quickly prefixing a list of columns with a string: -```mysql -a.column1, a.column2, a.column3, a.column4 -``` - -#### Parameters - -| Parameter | Description | Type | Required? | -| ---------------- | ----------------------------- | ------ | -------------------------------------------------------- | -| columns | A list of column names | List | check_circle | -| prefix_str | The prefix for the columns | String | check_circle | - -#### Usage - -```yaml -{{ dbtvault.prefix(['CUSTOMERKEY', 'DOB', 'NAME', 'PHONE'], 'a') }} -{{ dbtvault.prefix(['CUSTOMERKEY'], 'a') -``` - -!!! Note - Single columns must be provided as a 1-item list, as in the second example above. - -#### Output - -```mysql -a.CUSTOMERKEY, a.DOB, a.NAME, a.PHONE -a.CUSTOMERKEY -``` - -___ - -## Internal and Internal Deprecated -######(macros/internal) -######(macros/internal_deprecated) - -Internal macros support the other macros provided in this package. -They are used to process provided metadata and should not be called directly. - -## Table templates (deprecated) -######(macros/tables_deprecated) - -!!! warning "Deprecated" - The macros in this section are now deprecated as of v0.5, in favour of more streamlined metadata declaration and - usability. We have also removed raw vault column aliasing as this was bad practice. - -### hub_template - -Generates sql to build a hub table using the provided metadata. - -```mysql -dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) -``` - -#### Parameters - -| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | -| ------------- | --------------------------------------------------- | -------------------- | --------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | check_circle | -| src_nk | Source natural key column | String | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | -| tgt_nk | Target natural key column | List/Reference | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | -| source | Staging model reference or table name | List | List | check_circle | - -#### Usage - -``` yaml tab="Single-Source" - --- hub_customer.sql: - -{{- config(...) -}} - -{%- set source = [ref('stg_customer_hashed')] -%} - . -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_nk = 'CUSTOMER_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_nk = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} -``` - -``` yaml tab="Union" - --- hub_parts.sql: - -{{- config(...) -}} - -{%- set source = [ref('stg_parts_hashed'), - ref('stg_supplier_hashed'), - ref('stg_lineitem_hashed')] -%} - -{%- set src_pk = 'PART_PK' -%} -{%- set src_nk = 'PART_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_nk = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} -``` - - -#### Output - -```mysql tab="Single-Source" -SELECT DISTINCT - CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, - CAST(stg.CUSTOMER_ID AS VARCHAR(38)) AS CUSTOMER_ID, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE -FROM ( - SELECT a.CUSTOMER_PK, a.CUSTOMER_ID, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_customer_hashed AS a -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.hub_customer AS tgt -ON stg.CUSTOMER_PK = tgt.CUSTOMER_PK -WHERE tgt.CUSTOMER_PK IS NULL -``` - -```mysql tab="Union" -SELECT DISTINCT - CAST(stg.PART_PK AS BINARY(16)) AS PART_PK, - CAST(stg.PART_ID AS NUMBER(38,0)) AS PART_ID, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE -FROM ( - SELECT src.PART_PK, src.PART_ID, src.LOADDATE, src.SOURCE, - LAG(SOURCE, 1) - OVER(PARTITION by PART_PK - ORDER BY PART_PK) AS FIRST_SOURCE - FROM ( - SELECT a.PART_PK, a.PART_ID, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_parts_hashed AS a - UNION - SELECT b.PART_PK, b.PART_ID, b.LOADDATE, b.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_supplier_hashed AS b - UNION - SELECT c.PART_PK, c.PART_ID, c.LOADDATE, c.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_lineitem_hashed AS c - ) as src -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.hub_parts AS tgt -ON stg.PART_PK = tgt.PART_PK -WHERE tgt.PART_PK IS NULL -AND stg.FIRST_SOURCE IS NULL -``` - -___ - -### link_template - -Generates sql to build a link table using the provided metadata. - -```mysql -dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) -``` - -#### Parameters - -| Parameter | Description | Type (Single-Source) | Type (Union) | Required? | -| ------------- | --------------------------------------------------- | ---------------------| ---------------------| ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | String | check_circle | -| src_fk | Source foreign key column(s) | List | List | check_circle | -| src_ldts | Source loaddate timestamp column | String | String | check_circle | -| src_source | Name of the column containing the source ID | String | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | List/Reference | check_circle | -| tgt_fk | Target foreign key column | List/Reference | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | List/Reference | check_circle | -| source | Staging model reference or table name | List | List | check_circle | - -#### Usage - -``` yaml tab="Single-Source" - --- link_customer_nation.sql: - -{{- config(...) -}} - -{%- set source = [ref('stg_crm_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} - -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} -``` - -``` yaml tab="Union" - --- link_customer_nation_union.sql: - -{{- config(...) -}} - -{%- set source = [ref('stg_sap_customer_hashed'), - ref('stg_crm_customer_hashed'), - ref('stg_web_customer_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_NATION_PK' -%} -{%- set src_fk = ['CUSTOMER_PK', 'NATION_PK'] -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = [['CUSTOMER_PK', 'BINARY(16)', 'CUSTOMER_FK'], - ['NATION_PK', 'BINARY(16)', 'NATION_FK']] -%} - -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) }} -``` - -#### Output - -```mysql tab="Single-Source" -SELECT DISTINCT - CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, - CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, - CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE -FROM ( - SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS a -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation AS tgt -ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK -WHERE tgt.CUSTOMER_NATION_PK IS NULL -``` - -```mysql tab="Union" -SELECT DISTINCT - CAST(stg.CUSTOMER_NATION_PK AS BINARY(16)) AS CUSTOMER_NATION_PK, - CAST(stg.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_FK, - CAST(stg.NATION_PK AS BINARY(16)) AS NATION_FK, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR(15)) AS SOURCE -FROM ( - SELECT src.CUSTOMER_NATION_PK, src.CUSTOMER_PK, src.NATION_PK, src.LOADDATE, src.SOURCE, - LAG(SOURCE, 1) - OVER(PARTITION by CUSTOMER_NATION_PK - ORDER BY CUSTOMER_NATION_PK) AS FIRST_SOURCE - FROM ( - SELECT a.CUSTOMER_NATION_PK, a.CUSTOMER_PK, a.NATION_PK, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_sap_customer_hashed AS a - UNION - SELECT b.CUSTOMER_NATION_PK, b.CUSTOMER_PK, b.NATION_PK, b.LOADDATE, b.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_crm_customer_hashed AS b - UNION - SELECT c.CUSTOMER_NATION_PK, c.CUSTOMER_PK, c.NATION_PK, c.LOADDATE, c.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_web_customer_hashed AS c - ) AS src -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.link_customer_nation_union AS tgt -ON stg.CUSTOMER_NATION_PK = tgt.CUSTOMER_NATION_PK -WHERE tgt.CUSTOMER_NATION_PK IS NULL -AND stg.FIRST_SOURCE IS NULL -``` - -___ - -### sat_template - -Generates sql to build a satellite table using the provided metadata. - -```mysql -dbtvault.sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - source) -``` - -#### Parameters - -| Parameter | Description | Type | Required? | -| ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_hashdiff | Source hashdiff column | String | check_circle | -| src_payload | Source payload column(s) | List | check_circle | -| src_eff | Source effective from column | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | check_circle | -| tgt_hashdiff | Target hashdiff column | List/Reference | check_circle | -| tgt_payload | Target payload column | List/Reference | check_circle | -| tgt_eff | Target effective from column | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | -| source | Staging model reference or table name | List/Reference | check_circle | - -#### Usage - - -``` yaml - --- sat_customer_details.sql: - -{{- config(...) -}} - -{%- set source = [ref('stg_customer_details_hashed')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_hashdiff = 'CUSTOMER_HASHDIFF' -%} -{%- set src_payload = ['CUSTOMER_NAME', 'CUSTOMER_DOB', 'CUSTOMER_PHONE'] -%} - -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} - -{%- set tgt_hashdiff = [ src_hashdiff , 'BINARY(16)', 'HASHDIFF'] -%} - -{%- set tgt_payload = [[src_payload[0], 'VARCHAR(60)', 'NAME'], - [src_payload[1], 'DATE', 'DOB'], - [src_payload[2], 'VARCHAR(15)', 'PHONE']] -%} - -{%- set tgt_eff = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - source) }} -``` - - -#### Output - -```mysql -SELECT DISTINCT - CAST(e.CUSTOMER_HASHDIFF AS BINARY(16)) AS HASHDIFF, - CAST(e.CUSTOMER_PK AS BINARY(16)) AS CUSTOMER_PK, - CAST(e.CUSTOMER_NAME AS VARCHAR(60)) AS NAME, - CAST(e.CUSTOMER_DOB AS DATE) AS DOB, - CAST(e.CUSTOMER_PHONE AS VARCHAR(15)) AS PHONE, - CAST(e.LOADDATE AS DATE) AS LOADDATE, - CAST(e.EFFECTIVE_FROM AS DATE) AS EFFECTIVE_FROM, - CAST(e.SOURCE AS VARCHAR(15)) AS SOURCE -FROM MYDATABASE.MYSCHEMA.stg_customer_details_hashed AS e -LEFT JOIN ( - SELECT d.CUSTOMER_PK, d.HASHDIFF, d.NAME, d.DOB, d.PHONE, d.EFFECTIVE_FROM, d.LOADDATE, d.SOURCE - FROM ( - SELECT c.CUSTOMER_PK, c.HASHDIFF, c.NAME, c.DOB, c.PHONE, c.EFFECTIVE_FROM, c.LOADDATE, c.SOURCE, - CASE WHEN RANK() - OVER (PARTITION BY c.CUSTOMER_PK - ORDER BY c.LOADDATE DESC) = 1 - THEN 'Y' ELSE 'N' END CURR_FLG - FROM ( - SELECT a.CUSTOMER_PK, a.HASHDIFF, a.NAME, a.DOB, a.PHONE, a.EFFECTIVE_FROM, a.LOADDATE, a.SOURCE - FROM MYDATABASE.MYSCHEMA.sat_customer_details as a - JOIN MYDATABASE.MYSCHEMA.stg_customer_details_hashed as b - ON a.CUSTOMER_PK = b.CUSTOMER_PK - ) as c - ) AS d -WHERE d.CURR_FLG = 'Y') AS src -ON src.HASHDIFF = e.CUSTOMER_HASHDIFF -WHERE src.HASHDIFF IS NULL -``` - -___ - -### t_link_template - -Generates sql to build a transactional link table using the provided metadata. - -```mysql -dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, - source) -``` - -#### Parameters - -| Parameter | Description | Type | Required? | -| ------------- | --------------------------------------------------- | -------------- | ------------------------------------------------------------------ | -| src_pk | Source primary key column | String | check_circle | -| src_fk | Source foreign key column(s) | List | check_circle | -| src_payload | Source payload column(s) | List | check_circle | -| src_eff | Source effective from column | String | check_circle | -| src_ldts | Source loaddate timestamp column | String | check_circle | -| src_source | Name of the column containing the source ID | String | check_circle | -| tgt_pk | Target primary key column | List/Reference | check_circle | -| tgt_fk | Target hashdiff column | List/Reference | check_circle | -| tgt_payload | Target foreign key column(s) | List/Reference | check_circle | -| tgt_eff | Target effective from column | List/Reference | check_circle | -| tgt_ldts | Target loaddate timestamp column | List/Reference | check_circle | -| tgt_source | Name of the column which will contain the source ID | List/Reference | check_circle | -| source | Staging model reference or table name | List/Reference | check_circle | - -#### Usage - - -``` yaml - --- t_link_transactions.sql: - -{{- config(...) -}} - -{%- set source = [ref('stg_transactions_hashed')] -%} - -{%- set src_pk = 'TRANSACTION_PK' -%} -{%- set src_fk = ['CUSTOMER_FK', 'ORDER_FK'] -%} -{%- set src_payload = ['TRANSACTION_NUMBER', 'TRANSACTION_DATE', 'TYPE', 'AMOUNT'] -%} -{%- set src_eff = 'EFFECTIVE_FROM' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_fk = source -%} -{%- set tgt_payload = source -%} -{%- set tgt_eff = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, - source) }} -``` - -#### Output - -```mysql -SELECT DISTINCT - CAST(stg.TRANSACTION_PK AS BINARY) AS TRANSACTION_PK, - CAST(stg.CUSTOMER_FK AS BINARY) AS CUSTOMER_FK, - CAST(stg.ORDER_FK AS BINARY) AS ORDER_FK, - CAST(stg.TRANSACTION_NUMBER AS NUMBER(38,0)) AS TRANSACTION_NUMBER, - CAST(stg.TRANSACTION_DATE AS DATE) AS TRANSACTION_DATE, - CAST(stg.TYPE AS VARCHAR) AS TYPE, - CAST(stg.AMOUNT AS NUMBER(12,2)) AS AMOUNT, - CAST(stg.EFFECTIVE_FROM AS DATE) AS EFFECTIVE_FROM, - CAST(stg.LOADDATE AS DATE) AS LOADDATE, - CAST(stg.SOURCE AS VARCHAR) AS SOURCE -FROM ( - SELECT stg.TRANSACTION_PK, stg.CUSTOMER_FK, stg.ORDER_FK, stg.TRANSACTION_NUMBER, stg.TRANSACTION_DATE, stg.TYPE, stg.AMOUNT, stg.EFFECTIVE_FROM, stg.LOADDATE, stg.SOURCE - FROM MYDATABASE.MYSCHEMA.stg_transactions_hashed AS stg -) AS stg -LEFT JOIN MYDATABASE.MYSCHEMA.t_link_transactions AS tgt -ON stg.TRANSACTION_PK = tgt.TRANSACTION_PK -WHERE tgt.TRANSACTION_PK IS NULL -``` -___ diff --git a/docs/metadata.md b/docs/metadata.md deleted file mode 100644 index 3d1bca504..000000000 --- a/docs/metadata.md +++ /dev/null @@ -1,161 +0,0 @@ -As of v0.5, metadata is provided to the models through the ```dbt_project.yml``` file instead of being specified in -the models themselves. This keeps the metadata all in one place and simplifies the use of dbtvault. - -For further detail of the below table templates, see: [table templates](macros.md#table-templates). - -!!! note - In v0.5, only source column metadata is necessary, we have removed target column metadata. - -#### Declaring sources (in the metadata) - -Since v0.5, there is no longer the need to state the source using the ```ref``` macro, the new [macros](macros.md) do this all for -you. For single source models, just state the name of the source as a string. -For the case of union models, just state the sources as a list of strings. Examples of both of these can be seen below: - -```yaml tab="Single Source" -hub_customer: - vars: - source: 'v_stg_orders' -``` - -```yaml tab="Union" -hub_nation: - vars: - source: - - 'v_stg_orders' - - 'v_stg_inventory' -``` - -#### Hubs - -Only the source metadata is needed to build a hub, as column types and names are retained are retained in the target -table. The parameters that the [hub](macros.md#hub) macro accept are: - -| Parameter | Description | -| -------------| ---------------------------------------------------------| -| source | The staging table that feeds the hub. This can be a single source or a union. | -| src_pk | The column to use for the primary key (should be hashed) | -| src_nk | The natural key column that the primary key is based on. | -| src_ldts | The loaddate timestamp column of the record. | -| src_source | The source column of the record. | - -An example of the metadata structure for a hub is: - -```dbt_project.yml``` -```yaml -hub_customer: - vars: - source: 'stg_customer_hashed' - src_pk: 'CUSTOMER_PK' - src_nk: 'CUSTOMER_KEY' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' -``` - -#### Links - -The link metadata is very similar to the hub metadata. The parameters that the [link](macros.md#link) macro accept are: - -| Parameter | Description | -| -------------| ---------------------------------------------------------| -| source | The staging table that feeds the link. This can be single source or a union. | -| src_pk | The column to use for the primary key (should be hashed) | -| src_fk | The foreign key columns that the make up the primary link key. This must be entered as a list of strings. | -| src_ldts | The loaddate timestamp column of the record. | -| src_source | The source column of the record. | - -An example of the metadata structure for a link is: - -```dbt_project.yml``` -```yaml -link_customer_nation: - vars: - source: 'v_stg_orders' - src_pk: 'LINK_CUSTOMER_NATION_PK' - src_fk: - - 'CUSTOMER_PK' - - 'NATION_PK' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' -``` - -#### Satellites - -The metadata for satellites are different from that of links and hubs. The parameters the [sat](macros.md#sat) macro -accepts is: - -| Parameter | Description | -| -------------| ------------------------------------------------------------------- | -| source | The staging table that feeds the satellite (only single sources are used for satellites). | | -| src_pk | The primary key column of the table the satellite hangs off. | -| src_hashdiff | The hashdiff column of the satellite's payload. | -| src_payload | The columns that make up the payload of the satellite and are used in the hashdiff. The columns must be entered as a list of strings. | -| src_eff | The effective from date column. | -| src_ldts | The loaddate timestamp column of the record. | -| src_source | The source column of the record. | - -An example of the metadata structure for a satellite is: - -```dbt_project.yml``` -```yaml -sat_order_customer_details: - vars: - source: 'v_stg_orders' - src_pk: 'CUSTOMER_PK' - src_hashdiff: 'CUSTOMER_HASHDIFF' - src_payload: - - 'NAME' - - 'ADDRESS' - - 'PHONE' - - 'ACCBAL' - - 'MKTSEGMENT' - - 'COMMENT' - src_eff: 'EFFECTIVE_FROM' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' -``` - -#### Transactional links (non-historized links) - -The [t_link](macros.md#t_link) macro accepts the following parameters: - -| Parameter | Description | -| -------------| ------------------------------------------------------------------- | -| source | The staging table that feeds the transactional link (only single sources are used for transactional links). | -| src_pk | The primary key column of the transactional link. | -| src_fk | The foreign key columns that the make up the primary link key. This must be enter as a list of strings | -| src_payload | The columns that make up and payload of the transactional link. The columns must be entered as a list of strings. | -| src_eff | The effective from date column. | -| src_ldts | The loaddate timestamp column of the record. | -| src_source | The source column of the record. | - -```dbt_project.yml``` -```yaml -t_link_transactions: - vars: - source: 'v_stg_transactions' - src_pk: 'TRANSACTION_PK' - src_fk: - - 'CUSTOMER_FK' - - 'ORDER_FK' - src_payload: - - 'TRANSACTION_NUMBER' - - 'TRANSACTION_DATE' - - 'TYPE' - - 'AMOUNT' - src_eff: 'EFFECTIVE_FROM' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' -``` - -#### Effectivity satellites - -Documentation coming soon. Please refer to [eff_sat](macros.md#eff_sat) in the meantime. - -#### The problem with metadata - -As metadata is stored in the ```dbt_project.yml```, you can probably foresee the file getting very large for bigger -projects. To help manage large amounts of metadata, we recommend the use of external licence-based tools such as WhereScape, -Matillion, and erwin Data Modeller. We have future plans to improve metadata handling but in the meantime -any feedback or ideas are welcome. -___ \ No newline at end of file diff --git a/docs/migrating_v0.4_v0.5.md b/docs/migrating_v0.4_v0.5.md deleted file mode 100644 index e05d434a4..000000000 --- a/docs/migrating_v0.4_v0.5.md +++ /dev/null @@ -1,52 +0,0 @@ -# Migrating from v0.4 to v0.5 - -With the release of v0.5, we moved the metadata into variables held in in the ```dbt_project.yml``` file. -Your old metadata would have looked something like this: - -```sql -{{- config(materialized='incremental', schema='vlt', enabled=true, tags='hubs') -}} - -{%- set source = [ref('v_stg_orders')] -%} - -{%- set src_pk = 'CUSTOMER_PK' -%} -{%- set src_nk = 'CUSTOMER_ID' -%} -{%- set src_ldts = 'LOADDATE' -%} -{%- set src_source = 'SOURCE' -%} - -{%- set tgt_pk = source -%} -{%- set tgt_nk = source -%} -{%- set tgt_ldts = source -%} -{%- set tgt_source = source -%} - -{{ dbtvault.hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) }} -``` - -With v0.5, several things have changed: - - - The metadata is now specified in the ```dbt_project.yml``` file. Below is how to structure this metadata in -the ```dbt_project.yml``` file. -- You can no longer specify target column mappings, your target table columns -will be derived from your source table metadata. - -The metadata is structured as follows in the ```dbt_project.yml``` file: - -```yaml -hub_customer: - vars: - source: 'v_stg_orders' - src_pk: 'CUSTOMER_PK' - src_nk: 'CUSTOMER_KEY' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' -``` - -The new example ```hub_customer.sql``` would then look like: - -```sql -{{- config(materialized='incremental', schema='MYSCHEMA', tags='hub') -}} - -{{ dbtvault.hub(var('src_pk'), var('src_nk'), var('src_ldts'), - var('src_source'), var('source')) }} -``` \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index bca70311e..000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -mkdocs==1.1 -mkdocs-material==4.4.2 -mkdocs-minify-plugin==0.2.3 -pygments==2.6.1 -pymdown-extensions==6.3 \ No newline at end of file diff --git a/docs/roadmap.md b/docs/roadmap.md deleted file mode 100644 index 2dd10d3c7..000000000 --- a/docs/roadmap.md +++ /dev/null @@ -1,42 +0,0 @@ -With each release we will be adding more Data Vault 2.0 table templates, helpful macros and productivity enhancements. -We hope to tailor new features to the requirements of our community, making the package -the best and most useful it can be. - -We will be releasing changes incrementally, so you can reap the benefits as soon as features are developed. - -#### Contribute to dbtvault - -- Do you have some ideas? [Let us know what you want added](https://github.com/Datavault-UK/dbtvault/issues) -- Want to contribute your own work? [Read our contribution guidelines](https://github.com/Datavault-UK/dbtvault/blob/master/CONTRIBUTING.md) - -## Coming soon - -These features are currently planned for the near-future, -and are available now in a beta release (v0.6b2) - -- Effectivity satellites, [try it out now!](changelog_beta.md) - -## Future releases - -In future releases, we hope to include the following: - -### Tables - -- Multi-active satellites -- Status tracking satellites -- Point-in-Time tables (also know as PITs) -- Bridge tables -- Reference Tables -- Mart loading helpers -- Custom materialization for periodic loading similar to the -[dbt_utils offering for Redshift](https://github.com/fishtown-analytics/dbt-utils/blob/master/README.md#insert_by_period-source) -- And more! - -### Improvements - -- Staging re-work (move to YAML) - -### Additional features - -- Auditing -- Logging diff --git a/docs/satellites.md b/docs/satellites.md deleted file mode 100644 index cd3a393c4..000000000 --- a/docs/satellites.md +++ /dev/null @@ -1,128 +0,0 @@ -Satellites contain point-in-time payload data related to their parent hub or link records. -Each hub or link record may have one or more child satellite records, allowing us to record changes in -the data as they happen. - -They will usually consist of the following columns: - -1. A primary key (or surrogate key) which is usually a hashed representation of the natural key. - -2. A hashdiff. This is a concatenation of the payload (below) and the primary key. This -allows us to detect changes in a record. For example, if a customer changes their name, -the hashdiff will change as a result of the payload changing. - -3. A payload. The payload consists of concrete data for an entity, i.e. a customer record. This could be -a name, a date of birth, nationality, age, gender or more. The payload will contain some or all of the -concrete data for an entity, depending on the purpose of the satellite. - -4. An effectivity date. Usually called ```EFFECTIVE_FROM```, this column is the business effective date of a -satellite record. It records that a record is valid from a specific point in time. -If a customer changes their name, then the record with their 'old' name should no longer be valid, and it will no longer -have the most recent ```EFFECTIVE_FROM``` value. - -5. The load date or load date timestamp. This identifies when the record was first loaded into the vault. - -6. The source for the record. - -!!! note - ```LOADDATE``` is the time the record is loaded into the database. ```EFFECTIVE_FROM``` is different and may hold a - different value, especially if there is a batch processing delay between when a business event happens and the - record arriving in the database for load. Having both dates allows us to ask the questions 'what did we know when' - and 'what happened when' using the ```LOADDATE``` and ```EFFECTIVE_FROM``` date accordingly. - -### Creating the model header - -Create a new dbt model as before. We'll call this one ```sat_customer_details```. - -The following header is what we use, but feel free to customise it to your needs: - -```sat_customer_details.sql``` -```sql -{{- config(materialized='incremental', schema='MYSCHEMA', tags='sat') -}} -``` - -Satellites are always incremental, as we load and add new records to the existing data set. - -[Read more about incremental models](https://docs.getdbt.com/v0.15.0/docs/configuring-incremental-models) - -### Adding the metadata - -Let's look at the metadata we need to provide to the [sat](macros.md#sat) macro via the ```dbt_project.yml``` file. - -#### Source table - -The first piece of metadata we need is the source table. This step is easy, as in this example we created the -staging layer ourselves. All we need to do is provide the name of stage table as a string in our metadata -as follows. - -```dbt_project.yml``` -```yaml -sat_customer_details: - vars: - source: 'stg_customer_hashed' -``` - -#### Source columns - -Next, we define the columns which we would like to bring from the source. -Using our knowledge of what columns we need in our ```sat_customer_details``` table, we can identify columns in our -staging layer which map to them: - -1. The primary key of the parent hub or link table, which is a hashed natural key. -The ```CUSTOMER_PK``` we created earlier in the [staging](staging.md) section will be used for ```sat_customer_details```. -2. A hashdiff. We created ```CUSTOMER_HASHDIFF``` in [staging](staging.md) earlier, which we will use here. -3. Some payload columns: ```CUSTOMER_NAME```, ```CUSTOMER_DOB```, ```CUSTOMER_PHONE``` which should be present in the -raw staging layer via an [add_columns](macros.md#add_columns) macro call. -4. An ```EFFECTIVE_FROM``` column, also added in staging. -5. A load date timestamp, which is present in the staging layer as ```LOADDATE```. -6. A ```SOURCE``` column. - -We can now add this metadata to the ```dbt_project```: - -```dbt_project.yml``` -```yaml hl_lines="4 5 6 7 8 9 10 11 12" -sat_order_customer_details: - vars: - source: 'stg_customer_hashed' - src_pk: 'CUSTOMER_PK' - src_hashdiff: 'CUSTOMER_HASHDIFF' - src_payload: - - 'CUSTOMER_NAME' - - 'CUSTOMER_DOB' - - 'CUSTOMER_PHONE' - src_eff: 'EFFECTIVE_FROM' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' -``` - -### Invoking the template - -Now we bring it all together and call the [sat](macros.md#sat_) macro: - -```sat_customer_details.sql``` -```sql hl_lines="3 4 5" -{{- config(materialized='incremental', schema='MYSCHEMA', tags='satellite') -}} - -{{ dbtvault.sat(var('src_pk'), var('src_hashdiff'), var('src_payload'), - var('src_eff'), var('src_ldts'), var('src_source'), - var('source')) }} -``` - -### Running dbt - -With our model complete, we can run dbt to create our ```sat_customer_details``` satellite. - -```dbt run --models +sat_customer_details``` - -And our table will look like this: - -| CUSTOMER_PK | CUSTOMER_HASHDIFF | CUSTOMER_NAME | CUSTOMER_DOB | CUSTOMER_PHONE | EFFECTIVE_FROM | LOADDATE | SOURCE | -| ------------ | ------------ | ---------- | ------------ | --------------- | -------------- | ----------- | ------ | -| B8C37E... | 3C5984... | Alice | 1997-04-24 | 17-214-233-1214 | 1993-01-01 | 1993-01-01 | 1 | -| . | . | . | . | . | . | . | 1 | -| . | . | . | . | . | . | . | 1 | -| FED333... | D8CB1F... | Dom | 2018-04-13 | 17-214-233-1217 | 1993-01-01 | 1993-01-01 | 1 | - - -### Next steps - -We have now created a staging layer and a hub, link and satellite. Next we will look at [transactional links](t_links.md). \ No newline at end of file diff --git a/docs/setup.md b/docs/setup.md deleted file mode 100644 index d6518e70d..000000000 --- a/docs/setup.md +++ /dev/null @@ -1,188 +0,0 @@ -## Download the demonstration project - -Assuming you already have a python environment installed, the next step is to download the latest -demonstration project from the repository. - -Using the button below, find the latest release and download the zip file, listed under assets. - - - View Downloads - - -Once downloaded, unzip the project. - -## Installing requirements - -Once you have downloaded the project, install all of the requirements from the provided ```requirements.txt``` file. -First make sure the ```requirements.txt``` file is in your current working directory, then run: - -```pip install -r requirements.txt``` - -This will install dbt and all of its dependencies, ready for -development with dbt. - -## Install dbtvault - -Next, we need to install dbtvault. -dbtvault has already been added to the ```packages.yml``` file provided with the example project, so all you need to do -is run the following command: - -```dbt deps``` - -## Setting up dbtvault with Snowflake - -In the provided dbt project file (```dbt_project.yml```) the profile is named ```snowflake-demo```. -In your dbt profiles, you must create a connection with this name and provide the snowflake -account details so that dbt can connect to your Snowflake databases. - -dbt provides their own documentation on how to configure profiles, so we suggest reading that -[here](https://docs.getdbt.com/v0.15.0/docs/configure-your-profile). - -A sample profile configuration is provided below which will get you started: - -```profiles.yml``` -```yaml -snowflake-demo: - target: dev - outputs: - dev: - type: snowflake - account: - - user: - password: - - role: - database: DV_PROTOTYPE_DB - warehouse: DV_PROTOTYPE_WH - schema: DEMO - threads: 4 - client_session_keep_alive: False -``` - -Replace everything in this configuration marked with```<>``` with your own Snowflake account details. - -Key points: - -- You must also create a ```DV_PROTOTYPE_DB``` database and ```DV_PROTOTYPE_WH``` warehouse. - - - -- Your ```DV_PROTOTYPE_WH``` warehouse should be X-Small in size and have a 5 minute auto-suspend, as we will -not be coming close to the limits of what Snowflake can process. - - - -- The role can be anything as long as it has full rights to the above schema and database, so we suggest the -default ```SYSADMIN```. - -- We have set ```threads``` to 4 here. This setting dictates how -many models are processed in parallel. In our experience, 4 is a reasonable amount and the full system is created in a -reasonable time-frame, however, you may run with as many threads as required. - -![alt text](./assets/images/database.png "Creating a database in snowflake") -![alt text](./assets/images/warehouse.png "Creating a warehouse in snowflake") - -## The project file - -As of v0.5, the ```dbt_project.yml``` file is now used as a metadata store. Below is an example file showing the -metadata for a single instance of each of the current table types. - -```dbt_project.yml``` -```yaml - -models: - snowflakeDemo: - load: - schema: "VLT" - enabled: true - materialized: incremental - stage: - schema: "STG" - enabled: true - materialized: view - raw: - schema: "RAW" - enabled: true - materialized: incremental - hubs: - enabled: true - hub_customer: - vars: - source: 'v_stg_orders' - src_pk: 'CUSTOMER_PK' - src_nk: 'CUSTOMER_KEY' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' - ... - links: - enabled: true - link_customer_nation: - vars: - source: 'v_stg_orders' - src_pk: 'LINK_CUSTOMER_NATION_PK' - src_fk: - - 'CUSTOMER_PK' - - 'NATION_PK' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' - ... - sats: - enabled: true - sat_order_customer_details: - vars: - source: 'v_stg_orders' - src_pk: 'CUSTOMER_PK' - src_hashdiff: 'CUSTOMER_HASHDIFF' - src_payload: - - 'NAME' - - 'ADDRESS' - - 'PHONE' - - 'ACCBAL' - - 'MKTSEGMENT' - - 'COMMENT' - src_eff: 'EFFECTIVE_FROM' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' - ... - t_links: - enabled: true - t_link_transactions: - vars: - source: 'v_stg_transactions' - src_pk: 'TRANSACTION_PK' - src_fk: - - 'CUSTOMER_FK' - - 'ORDER_FK' - src_payload: - - 'TRANSACTION_NUMBER' - - 'TRANSACTION_DATE' - - 'TYPE' - - 'AMOUNT' - src_eff: 'EFFECTIVE_FROM' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' - ... - vars: - date: TO_DATE('1992-01-08') -``` - -#### models - -Here we are specifying that models in the ```load``` directory should be loaded in to the ```VLT``` -schema, and models in the sub-directories ```stage``` and ```source``` should have their own schemas, -```STG``` and ```SRC``` respectively. We have also specified that they are all enabled, as well -as their materialization. Many of these attributes are also provided in the files themselves and take -precedence over these settings anyway, this is just a design choice. - -#### table metadata - -The table metadata is now provided, as of v0.5, in the ```dbt_project.yml``` file as seen in the above example. -For each of your table models you must specify the metadata using the correct hierarchy. The metadata provided here is -for the ```hub_customer.sql```, ```link_customer_nation.sql```, and ```sat_order_customer_details.sql``` models. - -#### global vars - -On line 73, we have vars that will apply to all models. -To simulate day-feeds, we use a variable we have named ```date``` which is used in the ```SRC``` models to -load for a specific date. This is described in more detail in the [Profiling TPC-H](sourceprofile.md) section. \ No newline at end of file diff --git a/docs/sourceprofile.md b/docs/sourceprofile.md deleted file mode 100644 index d4fb1df52..000000000 --- a/docs/sourceprofile.md +++ /dev/null @@ -1,97 +0,0 @@ -We are using the [TPC-H benchmarking dataset provided by Snowflake](https://docs.snowflake.net/manuals/user-guide/sample-data-tpch.html) -to demonstrate dbtvault and showcase the Data Vault architecture running on Snowflake. - -The data comes in 4 different sizes, we will be using the smallest in this guide, TPCH_SF10 which -contains 60 million rows in its largest table. - -Our aim is to simulate day-feeds into the Data Vault to demonstrate the loading process in a production -environment. Before we begin, the data needs to be profiled to identify patterns in the data -that could be used to help build the Data Vault and create an accurate simulation. - -The below diagram describes the TPC-H system. - -![alt text](./assets/images/tpch.png "ERD for the TPC-H dataset") -(source: [TPC Benchmark H Standard Specification](http://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v2.17.1.pdf)) - - -### Date fields - -There are a total of four date fields in the data set. - -Three of these are found in the ```LINEITEM``` table: - -- ```SHIPDATE``` -- ```COMMITDATE``` -- ```RECEIPTDATE``` - -And one in the ```ORDERS``` table: - -- ```ORDERDATE``` - -Through querying the data, we discovered that the dates behave as expected and appear in chronological order -the majority of the time: ```ORDERDATE```, ```SHIPDATE```, ```RECEIPTDATE```, ```COMMITDATE```, with ```COMMITDATE``` -occasionally going against this pattern. - -This pattern allows us to simulate a system feed over multiple days, but we need to know the range of dates -for the simulation to be accurate. We queried the data to find the maximum and minimum ```ORDERDATE``` and work out the -difference between them. We found the dates spanned around 6.59 years, or 2405 days. - -### Relationships - -Working out relationships between tables and fields is a key step in mapping an existing system to Data Vault, -as it ensures an accurate model of the system is built. - -#### Orders and Suppliers - -We first looked at the relationship between orders and suppliers by doing inner joins on -the ```LINEITEM``` and ```ORDERS``` table, with the ```SUPPLIER``` table and counting the distinct suppliers for each order. -We discovered that it is a one to many relationship: an order can contain parts which are from different suppliers. - -#### Customers and Orders - -Next we looked at the relationship between customers and orders. We wanted to check whether any customers exist without orders. -We did this by doing a left outer join on the ```ORDERS``` table, with the ```CUSTOMER``` table and discovered that several -customers exist without orders. - -#### Transactions - -To create transactional links in the demonstration project, we needed to simulate transactions, as there are no suitable -or explicit transaction records present in the dataset. There are implied transactions however, as customers place orders. -To simulate a concrete transactions, we created a raw staging layer as a view, called -```raw_transactions``` and used the following fields: - -- Customer key -- Order key -- Order date -- Total price, aliased as Amount, to mean the order is paid off in full. -- Type, a generated column, using a random selection between ```CR``` or ```DR``` to mean a debit or credit to the customer. -- Transaction Date. A calculated column which is takes the order date and adds 20 days, to mean a customer paid 20 days -after their order was made. -- Transaction number. A calculated column created by concatenating the Order key, Customer key and order date and padding the -result with 0s to ensure the number is 24 digits long. - -The ```ORDERS``` and ```CUSTOMER``` tables are then joined (left outer) to simulate transactions on customer orders. - -### Conclusions - -To create a source feed simulation with the static data (shown by the logical pattern in the date fields), we can use -the ```ORDERDATE``` as a reference date. We can simulate historical data by only loading records before a particular -```ORDERDATE```. Any records in the history where the ```SHIPDATE```, ```RECEIPTDATE``` and ```COMMITDATE``` are after -reference ```ORDERDATE``` will be included but set to ```NULL``` to allow us to simulate existing records being updated -in a new day-feed. - -By profiling the relationships we have identified that the ```PARTSUPP``` table can more appropriately be referred to as -```INVENTORY```, since it is a static relationship (there is no date involved and therefore no changes). This means that -data involving the ```PARTSUPP```, ```SUPPLIER``` and ```PARTS``` tables create an inventory which can be linked -to the ```LINEITEM``` table. - -The relationship between customers and orders tells us that customers without an order will not be loaded into the Data -Vault, as we are using the ```ORDERDATE``` for day-feed simulation. - -This also means that we can simulate transactions by using the implication that a customer makes a payment on an order -some time after the order has been made. - -Now that we have profiled the data, we can make more informed decisions when mapping the source system to the Data Vault -architecture. - - diff --git a/docs/staging.md b/docs/staging.md deleted file mode 100644 index 9115dabe7..000000000 --- a/docs/staging.md +++ /dev/null @@ -1,223 +0,0 @@ -![alt text](./assets/images/staging.png "Staging from a raw table to the raw vault") - -The dbtvault package assumes you've already loaded a Snowflake database staging table with raw data -from a source system or feed (the 'raw staging layer'). - -### Pre-conditions - -There are a few conditions that need to be met for the dbtvault package to work: - -- All records are for the same ```load_datetime``` -- The table is truncated & loaded with data for each load cycle - -Instead of truncating and loading, you may also build a view over the table to filter out the right records and load -from the view. - -### Let's Begin - -The raw staging table needs to be pre-processed to add extra columns of data to make it ready to load to the raw vault. -Specifically, we need to add primary key hashes, hashdiffs, and any implied fixed-value columns (see the diagram). - -We also need to ensure column names align with target hub or link tables. - -!!! info - Hashing of primary keys is optional in Snowflake and natural keys alone can be used in place of hashing. - - We've implemented hashing as the only option for now, though a non-hashed version will be added in future releases. - -## Creating the stage model - -To prepare our raw staging layer for loading the vault, we create a dbt model and call dbtvault staging macros with -provided metadata. - -Our model will consist of: - -- a header -- a source table declaration -- metadata passed to staging macros. -- a footer - -### Creating the model header - -First we create a new dbt model. Our source table is called ```stg_customer``` -so we should name our additional layer ```stg_customer_hashed```, although any sensible naming convention will work if -kept consistent. In this case, we create a new file ```stg_customer_hashed.sql``` in our models folder. - -Let's start by adding the model header to the file: - -```stg_customer_hashed.sql``` -```sql - -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} - -``` - -This is a simple header block. You may add further tags if necessary, for your own needs, the important parts are the -materialization type and the schema name: - -- The ```materialized``` parameter defines how our table will be materialised in our database. -Usually we want hashing layers to be views, as they build upon the raw staging layer. -- The ```schema``` parameter is the name of the schema where this staging table will be created. - -### Setting the source table - -Next we will create a variable which holds a reference to the raw source table, since we will need to refer to it a few times -in our model. - -!!! note - On line 3 below we are using a dbt source. - - If you have not yet set up sources in your dbt configuration please refer to [setting up sources](walkthrough.md#setting-up-sources). - - -```stg_customer_hashed.sql``` -```sql hl_lines="3" - -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} - -{%- set source_table = source('MYSOURCE', 'stg_customer') -%} -``` - -### Generating hashes from metadata - -Now we get into the core component of staging: the metadata. -The metadata consists of the column names we want to use in our hash, to use as primary keys in our data vault or to use as -hashdiffs for satellites (see the DV 2.0 book for detail of what these are) and the alias for our new hash column. - -We need to call the [multi_hash](macros.md#multi_hash) macro and provide the appropriate parameters. The macro takes -our provided lists of columns, iterates through each of them, and generates all of the necessary SQL to create the hash for us. More on how to use this macro is -provided in the link above. - -After adding the macro call, our model will now look something like this: - -```stg_customer_hashed.sql``` -```sql hl_lines="5 6 7 8 9 10" - -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} - -{%- set source_table = source('MYSOURCE', 'stg_customer') -%} - -{{ dbtvault.multi_hash([('CUSTOMER_KEY', 'CUSTOMER_PK'), - ('NATION_KEY', 'NATION_PK'), - (['CUSTOMER_KEY', 'NATION_KEY'], 'CUSTOMER_NATION_PK'), - (['CUSTOMER_KEY', 'CUSTOMER_NAME', - 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], - 'CUSTOMER_HASHDIFF', true)]) -}}, -``` - -!!! note - Make sure you add the trailing comma after the call, at the end of line 9. - -This call will: - -- Hash the ```CUSTOMER_KEY``` column, and create a new column called ```CUSTOMER_PK``` containing the hash -value. -- Hash the ```NATION_KEY``` column, and create a new column called ```NATION_PK``` containing the hash -value. -- Concatenate the values in the ```CUSTOMER_KEY``` and ```NATION_KEY``` columns and hash them in the order supplied, creating a new -column called ```CUSTOMER_NATION_PK``` containing the hash of the combination of the values. -- Concatenate the values in the ```CUSTOMER_KEY```, ```CUSTOMER_NAME```, ```CUSTOMER_PHONE```, ```CUSTOMER_DOB``` -columns and hash them, creating a new column called ```CUSTOMER_NATION_PK``` containing the hash of the -combination of the values. The ```true``` parameter should be provided so that the columns are alpha-sorted. - -The latter three pairs will be used later when creating [links](links.md) and [satellites](satellites.md). - -### Additional columns - -With the [add_columns](macros.md#add_columns) macro, we can provide a list of columns and any corresponding aliases for -those columns. - -We now add the column names we want to bring forward/feed from the raw staging table into the raw vault. -To include all columns which exist in the source table, we provide the ```source_table``` variable we created earlier. - -We will also need to add some additional columns to our staging layer, containing 'constants' implied by the context of the -staging data. For example, we can add a source table code value for audit purposes, the load date, or some other constant needed in -the primary key. - -We can also override any columns coming in from the source, with different data. We may want to do this if a source -column already exists in the raw stage and the values aren't appropriate. - -We provide a constant by adding an ```!``` to the data and alias them with the same name as the column we want to -override. We can also use this method to create any new columns which do not already -exist in the source. - - -```stg_customer_hashed.sql``` -```sql hl_lines="12 13 14" - -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} - -{%- set source_table = source('MYSOURCE', 'stg_customer') -%} - -{{ dbtvault.multi_hash([('CUSTOMER_KEY', 'CUSTOMER_PK'), - ('NATION_KEY', 'NATION_PK'), - (['CUSTOMER_KEY', 'NATION_KEY'], 'CUSTOMER_NATION_PK'), - (['CUSTOMER_KEY', 'CUSTOMER_NAME', - 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], - 'CUSTOMER_HASHDIFF', true)]) -}}, - -{{ dbtvault.add_columns(source_table, - [('!1', 'SOURCE'), - ('LOADDATE', 'EFFECTIVE_FROM')]) }} - -``` - -In summary, above we have: - -- Added a header (line 1). -- Set the source_table variable to our raw staging table (line 3). -- Defined some hashing to create primary keys and a hashdiff (lines 5-10). -- Brought in all of the raw staging table's columns (line 12). -- Added a ```SOURCE``` column with the constant value ```1``` (line 13). -- Added an ```EFFECTIVE_FROM``` column which uses the ```LOADDATE``` value as its value (line 11). - -### Adding the footer - -Now we just need to provide the ```source_table``` variable we created earlier, as a parameter to the [from](macros.md#from) -macro. - -After adding the footer, our completed model should now look like this: - -```stg_customer_hashed.sql``` -```sql hl_lines="16" - -{{- config(materialized='view', schema='MYSCHEMA', enabled=true, tags='staging') -}} - -{%- set source_table = source('MYSOURCE', 'stg_customer') -%} - -{{ dbtvault.multi_hash([('CUSTOMER_KEY', 'CUSTOMER_PK'), - ('NATION_KEY', 'NATION_PK'), - (['CUSTOMER_KEY', 'NATION_KEY'], 'CUSTOMER_NATION_PK'), - (['CUSTOMER_KEY', 'CUSTOMER_NAME', - 'CUSTOMER_PHONE', 'CUSTOMER_DOB'], - 'CUSTOMER_HASHDIFF', true)]) -}}, - -{{ dbtvault.add_columns(source_table, - [('!1', 'SOURCE'), - ('LOADDATE', 'EFFECTIVE_FROM')]) }} - -{{ dbtvault.from(source_table) }} - -``` - -This model is now ready to run to create a view with all the added data/columns needed to load the raw vault. - -### Running dbt - -With our model complete, we can run dbt and have our new staging layer materialised as configured in the header: - -```dbt run --models stg_customer_hashed``` - -And our table will look like this: - -| CUSTOMER_PK | NATION_PK | CUSTOMER_NATION_PK | CUSTOMER_HASHDIFF | (source table columns) | EFFECTIVE_FROM | SOURCE | -| ------------ | ------------ | ------------------- | ------------------- | ---------------------- | -------------- | ------------ | -| B8C37E... | D89F3A... | 72A160... | . | . | 1993-01-01 | 1 | -| . | . | . | . | . | . | . | -| . | . | . | . | . | . | . | -| FED333... | D78382... | 1CE6A9... | . | . | 1993-01-01 | 1 | - -### Next steps - -Now that we have implemented a new staging layer with all of the required fields and hashes, we can start loading our vault -with hubs, links and satellites. \ No newline at end of file diff --git a/docs/stagingdemo.md b/docs/stagingdemo.md deleted file mode 100644 index cc1ea8ae2..000000000 --- a/docs/stagingdemo.md +++ /dev/null @@ -1,169 +0,0 @@ -![alt text](./assets/images/staging.png "Staging from a raw table to the raw vault") - -We have two staging layers, as shown in the diagram above. - -## The raw staging layer - -First we create a raw staging layer. This feeds in data from the source system so that we can process it -more easily. In the ```models/raw``` folder we have provided two models which set up a raw staging layer. - -### raw_orders - -The ```raw_orders``` model feeds data from TPC-H, into a wide table containing all of the orders data -for a single day-feed. The day-feed will load data from the day given in the ```date``` var. - -### raw_inventory - -The ```raw_inventory``` model feeds the static inventory from TPC-H. As this data does not contain any dates, -we do not need to do any additional date processing or use the ```date``` var as we did for the raw orders data. -The inventory consists of the ```PARTSUPP```, ```SUPPLIER```, ```PART``` and ```LINEITEM``` tables. - -### raw_transactions - -The ```raw_inventory``` simulates transactions so that we can create transactional links. It does this by -making a number of calculations on orders made by customers and creating transaction records. - -[Read more](sourceprofile.md#transactions) - -## Building the raw staging layer - -To build this layer with dbtvault, run the below command: - -```dbt run --models tag:raw``` - -Running this command will run all models which have the ``raw`` tag. We have given the ```raw``` tag to the -two raw staging layer models, so this will compile and run both models. - -The dbt output should give something like this: - -```shell -14:18:17 | Concurrency: 4 threads (target='dev') -14:18:17 | -14:18:17 | 1 of 3 START view model DEMO_RAW.raw_inventory....................... [RUN] -14:18:17 | 2 of 3 START view model DEMO_RAW.raw_orders.......................... [RUN] -14:18:17 | 3 of 3 START view model DEMO_RAW.raw_transactions.................... [RUN] -14:18:19 | 3 of 3 OK created view model DEMO_RAW.raw_transactions............... [SUCCESS 1 in 1.49s] -14:18:19 | 1 of 3 OK created view model DEMO_RAW.raw_inventory.................. [SUCCESS 1 in 1.71s] -14:18:20 | 2 of 3 OK created view model DEMO_RAW.raw_orders..................... [SUCCESS 1 in 2.06s] -14:18:20 | -14:18:20 | Finished running 3 view models in 8.10s. - -``` - -## The hashed staging layer - -The tables in the raw staging layer need to be processed to add extra columns of data to make it ready -to load to the raw vault. - -Specifically, we need to add primary key hashes, hashdiffs, and any implied fixed-value columns -(see the diagram at the top of the page). - -We have created a number of macros for dbtvault, to make this step easier. Below are some links to -the macro documentation to provide a deeper understanding of how the macros work. - -- [multi-hash](macros.md#multi_hash) Generates SQL for hashes from lists of column/alias pairs. -- [add-columns](macros.md#add_columns) Generates SQL for additional columns with constant or function-derived values, -from lists of column/alias pairs. -- [from](macros.md#from) Generates SQL for selecting from the source table. - -## The model header - -For the staging layers we use a header as follows: - -```sql -{{- config(materialized='view', schema='STG', enabled=true, tags='stage') -}} -``` - -This header is fairly-straight forward and defines the model as a view, as well as defining the schema as ```STG``` -to ensure that the location we are materializing this model in makes sense in the overall system. - -We also define the ```stage``` tag to categorise this model and make it easier to isolate when -we want to only run staging layer models. - -## The source table - -In the ```v_stg_orders``` model, we use set the following ``source_table``` variable: - -```sql -{%- set source_table = ref('raw_orders') -%} -``` - -This allows us to make use of the additional functionality of the [add-columns](macros.md#add_columns) macro -which will enable it to automatically bring in all columns from the defined ```source_table```. - -This will be very convenient for when we need to access the data when creating the raw vault later. - -## Hashing - -We provide a number of column/alias pairs to the [multi-hash](macros.md#multi_hash) macro -to generate hashing SQL. These hashes will be used in the raw vault tables as primary key -and hashdiff fields. - -!!! note "Why do we hash?" - For more information on why we hash, refer to the [best practices](bestpractices.md#why-do-we-hash) page. - -For hashdiff columns, we provide an additional parameter, ```sort``` with the value ```true``` to get -dbtvault to sort the columns alphabetically when hashing, as per best practices. - -## Additional columns - -We also provide a number of column/alias pairs to the [add-columns](macros.md#add_columns) macro -to generate SQL for adding additional columns to our hashed stage view. - -AS we mentioned before, if the ```source_table``` variable we created is provided as the first parameter, -all of the ```source_table``` columns will automatically be selected. - -If there are any constants which overlap with the ```source_table```, and the ```source_table``` has been -provided as a parameter, the constants provided to this macro will take precedence. - -This macro can crate any number of extra columns, which may contain values generated by database function calls -or contain constant values provided by you, the user. - -There's a simple shorthand method for providing constants which you can observe being used in the hashed -staging models. If we provide a ```!``` in the string value, it will create a column with that string -(minus the ```!```) as its value in every row. This is very useful when defining a source, -as you may want to force it to a certain value for auditing purposes. - -## From - -This is a simple convenience macro which generates SQL in the form ```FROM ```. - -## The hashed staging models - -### v_stg_orders and v_stg_inventory - -The ```v_stg_orders``` and ```v_stg_inventory``` models use the raw layer's ```raw_orders``` and ```raw_inventory``` -models as sources, respectively. Both are created as views on the raw staging layer, as they are intended as -transformations on the data which already exists. - -Each view adds a number of primary keys, hashdiffs and additional constants for use in the raw vault. - -### v_stg_transactions - -The ```v_stg_transactions``` model uses the raw layer's ```raw_transactions``` model as its source. -For the load date, we add a day to the ```TRANSACTION_DATE``` to simulate the fact we are loading the data in the date -after the transaction was made. - -## Building the hashed staging layer - -To build this layer with dbtvault, run the below command: - -```dbt run --models tag:stage``` - -Running this command will run all models which have the ``stage`` tag. We have given the ```stage``` tag to the -two hashed staging layer models, so this will compile and run both models. - -The dbt output should give something like this: - -```shell -14:19:17 | Concurrency: 4 threads (target='dev') -14:19:17 | -14:19:17 | 1 of 3 START view model DEMO_STG.v_stg_inventory..................... [RUN] -14:19:17 | 2 of 3 START view model DEMO_STG.v_stg_orders........................ [RUN] -14:19:17 | 3 of 3 START view model DEMO_STG.v_stg_transactions.................. [RUN] -14:19:19 | 3 of 3 OK created view model DEMO_STG.v_stg_transactions............. [SUCCESS 1 in 1.99s] -14:19:20 | 2 of 3 OK created view model DEMO_STG.v_stg_orders................... [SUCCESS 1 in 2.52s] -14:19:20 | 1 of 3 OK created view model DEMO_STG.v_stg_inventory................ [SUCCESS 1 in 2.59s] -14:19:20 | -14:19:20 | Finished running 3 view models in 7.98s. -``` \ No newline at end of file diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css deleted file mode 100644 index 8c8c6248c..000000000 --- a/docs/stylesheets/extra.css +++ /dev/null @@ -1,37 +0,0 @@ -/* Additional CSS to add styling to the navigation menu cube -and remove edit button */ - -.nav-cube img { - width: 90%; - padding: 0 .6rem; - margin-bottom: 20px; -} - -.md-content__icon { - display: none; -} - -/* Style buttons */ -.btn { - background-color: #91569D; - border: none; - color: white; - padding: 12px 30px; - cursor: pointer; - font-size: 20px; - -} - -.btn i, a.btn { - color: white; -} - -a.btn { - margin-top: 20px; - display: inline-block; -} - -/* Darker background on mouse-over */ -.btn:hover { - background-color: #7B4B87; -} \ No newline at end of file diff --git a/docs/t_links.md b/docs/t_links.md deleted file mode 100644 index 4c365e4e5..000000000 --- a/docs/t_links.md +++ /dev/null @@ -1,134 +0,0 @@ -# Transactional Links - -Also known as non-historized or no-history links, transactional links record the transaction or 'event' components of -their referenced hub tables. They allow us to model the more granular relationships between entities. Some prime examples -are purchases, flights or emails; there is a record in the table for every event or transaction between the entities -instead of just one record per relation. - -Our transactional links will contain: - -1. A primary key. For t-links, we take the natural keys (prior to hashing) represented by the foreign key columns below and create a hash on a concatenation of them. -2. Foreign keys holding the primary key for each hub referenced in the link (2 or more depending on the number of hubs referenced) -3. A payload. The payload consists of concrete data for an entity, i.e. a transaction record. This could be -a transaction number, an amount paid, transaction type or more. The payload will contain all of the -concrete data for a transaction. -4. An effectivity date. Usually called ```EFFECTIVE_FROM```, this column is the business effective date of a -satellite record. It records that a record is valid from a specific point in time. In the case of a transaction, this -is usually the date on which the transaction occured. - -5. The load date or load date timestamp. -6. The source for the record - -!!! note - ```LOADDATE``` is the time the record is loaded into the database. ```EFFECTIVE_FROM``` is different and may hold a - different value, especially if there is a batch processing delay between when a business event happens and the - record arriving in the database for load. Having both dates allows us to ask the questions 'what did we know when' - and 'what happened when' using the ```LOADDATE``` and ```EFFECTIVE_FROM``` date accordingly. - -### Creating the model header - -Create a new dbt model as before. We'll call this one ```t_link_transactions```. - -The following header is what we use, but feel free to customise it to your needs: - -```t_link_transactions.sql``` -```sql -{{- config(materialized='incremental', schema='MYSCHEMA', tags='t_link') -}} -``` - -Transactional links are always incremental, as we load and add new records to the existing data set. - -[Read more about incremental models](https://docs.getdbt.com/v0.15.0/docs/configuring-incremental-models) - -### Adding the metadata - -Let's look at the metadata we need to provide to the [t_link](macros.md#t_link) macro. - -#### Source table - -The first piece of metadata we need is the source table. For transactional links this can sometimes be a little -trickier than other table types. We need particular columns to model the transaction or event which has occured in the -relationship between the hubs we are referencing, and therefore may need to create a staging layer specifically for the -purposes of feeding the transactional link. - -For this step, ensure you have the following columns present in the source table: - -1. A hashed transaction number as the primary key -2. Hashed foreign keys, one for each of the referenced hubs. -3. A payload. This will be data about the transaction itself e.g. the amount, type, date or non-hashed transaction number. -4. An ```EFFECTIVE_FROM``` date. This will usually be the date of the transaction. -5. A load date timestamp -6. A source - -Assuming you have a raw source table with these required columns, we can create a hashed staging table -using a dbt model, (let's call it ```stg_transactions_hashed.sql```) and this is the table we reference in the -```dbt_project.yml``` file as a string. - -```dbt_project.yml``` -```yaml -t_link_transactions: - vars: - source: 'stg_transactions_hashed' - ... -``` - -#### Source columns - -Next, we define the columns which we would like to bring from the source. -We can use the columns we identified in the ```Source table``` section, above. - -```dbt_project.yml``` -```yaml hl_lines="4 5 6 7 8 9 10 11 12 13 14 15" -t_link_transactions: - vars: - source: 'stg_transactions_hashed' - src_pk: 'TRANSACTION_PK' - src_fk: - - 'CUSTOMER_FK' - - 'ORDER_FK' - src_payload: - - 'TRANSACTION_NUMBER' - - 'TRANSACTION_DATE' - - 'TYPE' - - 'AMOUNT' - src_eff: 'EFFECTIVE_FROM' - src_ldts: 'LOADDATE' - src_source: 'SOURCE' -``` - -### Invoking the template - -Now we bring it all together and call the [t_link](macros.md#t_link) macro: - -```t_link_transactions.sql``` -```sql hl_lines="3 4 5" -{{- config(materialized='incremental', schema='VLT', tags='t_link') -}} - -{{ dbtvault.t_link(var('src_pk'), var('src_fk'), var('src_payload'), - var('src_eff'), var('src_ldts'), var('src_source'), - var('source')) }} -``` - -### Running dbt - -With our model complete, we can run dbt to create our ```t_link_transactions``` transactional link. - -```dbt run --models +t_link_transactions``` - -And our table will look like this: - -| TRANSACTION_PK | CUSTOMER_FK | ORDER_FK | TRANSACTION_NUMBER | TYPE | AMOUNT | EFFECTIVE_FROM | LOADDATE | SOURCE | -| --------------- | ----------- | --------- | ------------------ | ---- | ------- | -------------- | ----------- | ------ | -| BDEE76... | CA02D6... | CF97F1... | 123456789101 | CR | 100.00 | 1993-01-28 | 1993-01-29 | 2 | -| . | . | . | . | . | . | . | . | . | -| . | . | . | . | . | . | . | . | . | -| E0E7A8... | F67DF4... | 2C95D4... | 123456789104 | CR | 678.23 | 1993-01-28 | 1993-01-29 | 2 | - - -### Next steps - -We have now created a staging layer and a hub, link, satellite and transactional link. We'll be bringing new -table structures in future releases. - -Take a look at our [worked example](workedexample.md) for a demonstration of a realistic environment with pre-written -models for you to experiment with and learn from. \ No newline at end of file diff --git a/docs/walkthrough.md b/docs/walkthrough.md deleted file mode 100644 index f5a34e1ed..000000000 --- a/docs/walkthrough.md +++ /dev/null @@ -1,76 +0,0 @@ -## Introduction - -!!! info - This walk-through intends to give you a detailed understanding of how to use - dbtvault and the provided macros to develop a Data Vault Data Warehouse from the ground up. - If you're looking to quickly experiment and learn using pre-written models, - take a look at our [worked example](workedexample.md). - -In this section we teach you how to use dbtvault step-by-step, explaining the use of macros and the -different components of the Data Vault in detail. - -We will: - -- process a raw staging layer. -- create a Data Vault with hubs, links and satellites using dbtvault. - -## Pre-requisites - -1. Some prior knowledge of Data Vault 2.0 architecture. Have a look at -[How can I get up to speed on Data Vault 2.0?](index.md#how-can-i-get-up-to-speed-on-data-vault-20) - -2. A Snowflake account, trial or otherwise. [Sign up for a free 30-day trial here](https://trial.snowflake.com/ab/) - -3. You must have downloaded and installed dbt 0.15.2, -and [set up a project](https://docs.getdbt.com/v0.15.0/docs/dbt-projects). - -4. Sources should be set up in dbt [(see below)](walkthrough.md#setting-up-sources). - -5. We assume you already have a raw staging layer. - -6. Our macros assume that you are only loading from one set of load dates in a single load cycle (i.e. your staging layer -contains data for one ```load_datetime``` value only). **We will be removing this restriction in future releases**. - -7. You should read our [best practices](bestpractices.md) guidance. - -## Setting up sources (in dbt) - -We will be using the ```source``` feature of dbt extensively throughout the documentation to make access to source -data much easier, cleaner and more modular. - -We have provided an example below which shows a configuration similar to that used for the examples in our documentation, -however this feature is documented extensively in [the documentation for dbt](https://docs.getdbt.com/v0.15.0/docs/using-sources). - -We recommend that you place the ```schema.yml``` file you create for your sources, -in the root of your ```models``` folder, however you can place it wherever needed for your specific project and models. - -```schema.yml``` - -```yaml -version: 2 - -sources: - - name: MYSOURCE - database: MYDATABASE - schema: MYSCHEMA - tables: - - name: stg_customer # alias - identifier: stg_customer_hashed # table name - - name: ... -``` - -## Installation - -Add the following to your ```packages.yml```: - -```yaml -packages: - - - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.5 -``` - -And run -```dbt deps``` - -[Read more on package installation (from dbt)](https://docs.getdbt.com/v0.15.0/docs/package-management) \ No newline at end of file diff --git a/docs/workedexample.md b/docs/workedexample.md deleted file mode 100644 index 292d07ef6..000000000 --- a/docs/workedexample.md +++ /dev/null @@ -1,59 +0,0 @@ -## Introduction - -!!! info - The intent behind this demonstration is to give you further understanding of how - dbt and dbtvault could be used in a realistic environment. - For a more detailed guide on how to create your own Data Vault using dbtvault, - with a simplified example, take a look at our [walk-through](walkthrough.md) guide. - -In this section we teach you how to use dbtvault by example. We guide you through developing a -Data Vault 2.0 Data Warehouse based on the Snowflake TPC-H dataset, step-by-step using pre-written dbtvault models. - -We will: - -- setup a dbt project. -- examine and profile the TPCH dataset to explore how we can map it to the Data Vault architecture. -- create a raw staging layer. -- process the raw staging layer. -- create a Data Vault with hubs, links, satellites and transactional links using dbtvault and pre-written models. - -## Pre-requisites - -These pre-requisites are separate from those found on the [getting started](walkthrough.md) page and will -be the only necessary requirements you will need to get started with the example project. - -1. Some prior knowledge of Data Vault 2.0 architecture. Have a look at -[How can I get up to speed on Data Vault 2.0?](index.md#how-can-i-get-up-to-speed-on-data-vault-20) - -2. A Snowflake trial account. [Sign up for a free 30-day trial here](https://trial.snowflake.com/ab/) - -3. A Python 3.x installation. - -!!! warning - We suggest a trial account so that you have full privileges and assurance that the demo is isolated from any - production warehouses. Whilst there is no risk that the demo affects any unrelated data outside of the - scope of this project, you will incur compute costs. - You may use a corporate account or existing personal account at your own risk. - -!!! note - We have provided a complete ```requirements.txt``` to install with ```pip install -r requirements.txt``` - as a quick way of getting your Python environment set up. This file includes dbt and comes with the download in the - next section. - -## Performance note - -Please be aware that table structures are simulated from the TPC-H dataset. The TPC-H dataset is a static view of data. - -Only a subset of the data contains dates which allows us to simulate daily feeds. The ```v_stg_orders``` orders view is -filtered by date, unfortunately the ```v_stg_inventory``` view cannot be filtered by date, so it ends up being a feed of -the entire contents of the view each cycle. - -This means that inventory related hubs, links and satellites are populated once during the initial load cycle with -everything and later cycles insert 0 new records in their left outer joins. - -As the dataset increases in size, e.g if you run with a larger TPC-H dataset (100, 1000 etc.) then be aware you are -processing the entire inventory dataset each cycle, which results in unrepresentative load cycle times. - -We have minimised the impact of this by adding a join in the raw inventory table on the raw orders table to ensure only -inventory items which are included in orders are fed into raw staging. The outcome is the same, but it significantly -optimises the loading process and thereby reduces load time. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 1a1f9782d..000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,79 +0,0 @@ -site_name: dbtvault -site_author: Datavault -site_dir: 'site' -repo_name: 'Datavault-UK/dbtvault' -repo_url: 'https://github.com/Datavault-UK/dbtvault' - -theme: - name: 'material' - custom_dir: 'theme' - show_sidebar: true - logo: 'assets/images/logo.png' - banner: 'assets/images/docs-banner.png' - favicon: 'assets/images/favicon.ico' - palette: - primary: 'black' - accent: 'indigo' - highlightjs: true - - -nav: - - Home: 'index.md' - - Metadata: 'metadata.md' - - Walk-through guide: - - Getting Started: 'walkthrough.md' - - Staging: 'staging.md' - - Hubs: 'hubs.md' - - Links: 'links.md' - - Satellites: 'satellites.md' - - Transactional Links: 't_links.md' - - Effectivity Satellites: 'eff_sats.md' - - Worked example: - - Getting Started: 'workedexample.md' - - Project setup: 'setup.md' - - Profiling TPC-H: 'sourceprofile.md' - - Creating the stage layers: 'stagingdemo.md' - - Loading the vault: 'loading.md' - - Macros: 'macros.md' - - Migration Guides: - - Migrating from v0.4 to v0.5: 'migrating_v0.4_v0.5.md' - - Best Practices: 'bestpractices.md' - - Roadmap: 'roadmap.md' - - Changelog: - - Stable Releases: 'changelog.md' - - Beta Releases: 'changelog_beta.md' - - Contributing: 'contributing.md' - - Licence: 'LICENSE.md' - -extra: - social: - - icon: 'fontawesome/solid/globe' - link: 'https://www.data-vault.co.uk' - - icon: 'fontawesome/brands/github' - link: 'https://github.com/Datavault-UK/' - - icon: 'fontawesome/brands/twitter' - link: 'https://twitter.com/datavault_uk' - - icon: 'fontawesome/brands/linkedin' - link: 'https://www.linkedin.com/company/business-thinking-limited' - - icon: 'fontawesome/brands/facebook' - link: 'https://www.facebook.com/DataVaultUK/' - version: 0.5 - req_dbt_version: 0.15.x - -markdown_extensions: - - codehilite: - linenums: true - - admonition - - pymdownx.superfences - - pymdownx.emoji - - toc: - permalink: true - toc_depth: 1-3 - -plugins: - - search - -extra_css: - - 'stylesheets/extra.css' - -copyright: dbtvault and documentation © Business Thinking trading as Datavault 2020 - Data Vault 2.0 is a registered trademark of Dan Linstedt - dbt is a registered trademark of Fishtown Analytics diff --git a/theme/main.html b/theme/main.html deleted file mode 100644 index 333faee70..000000000 --- a/theme/main.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends "base.html" %} - - -{% block site_nav %} - - -{% if nav %} -

- -
-
- {% include "partials/nav.html" %} -
-
-
-{% endif %} - - -{% if page.toc %} -
-
-
- {% include "partials/toc.html" %} -
-
-
-{% endif %} - -{% endblock %} \ No newline at end of file From f4097e9e114c13630fae5b440d42d85f3b4700b8 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Sun, 17 May 2020 18:47:44 +0100 Subject: [PATCH 129/157] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3fe9e0148..a51c293c4 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@

+[![CircleCI](https://circleci.com/gh/Datavault-UK/dbtvault-dev.svg?style=svg&circle-token=7b9d6cb90833b6953c82493162951aca0b12a75c)](https://app.circleci.com/pipelines/github/Datavault-UK/dbtvault-dev) [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) [![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) From a9d69f6cfa506b3f6a14a36bda8e90bc20a1c46d Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Sun, 17 May 2020 18:58:53 +0100 Subject: [PATCH 130/157] Update README.md Removed CircleCI badge temporarily --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index a51c293c4..27abae1fc 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@

- -[![CircleCI](https://circleci.com/gh/Datavault-UK/dbtvault-dev.svg?style=svg&circle-token=7b9d6cb90833b6953c82493162951aca0b12a75c)](https://app.circleci.com/pipelines/github/Datavault-UK/dbtvault-dev) [![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) [![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) From 392c34d419b086adffb3c7456eb8017c39e615af Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 01:12:07 +0100 Subject: [PATCH 131/157] Release 0.6 --- .github/ISSUE_TEMPLATE/bug_report.md | 30 ----- .github/ISSUE_TEMPLATE/feature_request.md | 20 --- .gitignore | 1 - README.md | 16 ++- dbt_project.yml | 2 +- macros/internal/alias.sql | 53 ++++++++ macros/internal/{single.sql => alias_all.sql} | 12 +- .../from.sql => internal/as_constant.sql} | 14 +- macros/internal/docs/internal_macros.md | 5 + .../internal/docs/internal_macros_schema.yml | 10 ++ .../expand_column_list.sql} | 35 +++-- macros/internal/get_src_col_list.sql | 41 ------ macros/internal/hash_check.sql | 24 ---- macros/internal/is_multi_source.sql | 41 ------ macros/internal/is_union.sql | 49 ------- macros/internal/multikey.sql | 18 +-- macros/internal/new_union.sql | 36 ------ macros/internal/retrieve_tgt_cols.sql | 101 --------------- macros/internal/source_columns.sql | 27 ---- macros/internal/validate_columns.sql | 24 ---- macros/internal_deprecated/create_source.sql | 28 ---- .../internal_deprecated/create_tgt_cols.sql | 101 --------------- macros/internal_deprecated/union.sql | 35 ----- macros/staging/add_columns.sql | 49 ------- macros/staging/derive_columns.sql | 79 ++++++++++++ macros/staging/hash_columns.sql | 43 ++++++ macros/staging/multi_hash.sql | 30 ----- macros/staging/stage.sql | 80 ++++++++++++ macros/supporting/cast.sql | 18 +-- macros/supporting/hash.sql | 44 ++++--- macros/supporting/prefix.sql | 56 ++++++-- macros/tables/eff_sat.sql | 114 ---------------- macros/tables/hub.sql | 89 +++++++++---- macros/tables/link.sql | 122 ++++++++++++------ macros/tables/sat.sql | 27 ++-- macros/tables/t_link.sql | 6 +- macros/tables_deprecated/hub_template.sql | 49 ------- macros/tables_deprecated/link_template.sql | 49 ------- macros/tables_deprecated/sat_template.sql | 61 --------- macros/tables_deprecated/t_link_template.sql | 44 ------- 40 files changed, 584 insertions(+), 1099 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .gitignore create mode 100644 macros/internal/alias.sql rename macros/internal/{single.sql => alias_all.sql} (69%) rename macros/{staging/from.sql => internal/as_constant.sql} (65%) create mode 100644 macros/internal/docs/internal_macros.md create mode 100644 macros/internal/docs/internal_macros_schema.yml rename macros/{internal_deprecated/get_col_list.sql => internal/expand_column_list.sql} (51%) delete mode 100644 macros/internal/get_src_col_list.sql delete mode 100644 macros/internal/hash_check.sql delete mode 100644 macros/internal/is_multi_source.sql delete mode 100644 macros/internal/is_union.sql delete mode 100644 macros/internal/new_union.sql delete mode 100644 macros/internal/retrieve_tgt_cols.sql delete mode 100644 macros/internal/source_columns.sql delete mode 100644 macros/internal/validate_columns.sql delete mode 100644 macros/internal_deprecated/create_source.sql delete mode 100644 macros/internal_deprecated/create_tgt_cols.sql delete mode 100644 macros/internal_deprecated/union.sql delete mode 100644 macros/staging/add_columns.sql create mode 100644 macros/staging/derive_columns.sql create mode 100644 macros/staging/hash_columns.sql delete mode 100644 macros/staging/multi_hash.sql create mode 100644 macros/staging/stage.sql delete mode 100644 macros/tables/eff_sat.sql delete mode 100644 macros/tables_deprecated/hub_template.sql delete mode 100644 macros/tables_deprecated/link_template.sql delete mode 100644 macros/tables_deprecated/sat_template.sql delete mode 100644 macros/tables_deprecated/t_link_template.sql diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 92604ced1..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[BUG] " -labels: bug -assignees: DVAlexHiggs - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Log files** -If applicable, provide dbt log files which include the problem. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 3448c4715..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[FEATURE] " -labels: '' -assignees: DVAlexHiggs - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 45ddf0ae3..000000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -site/ diff --git a/README.md b/README.md index 27abae1fc..d1921566e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,8 @@ -### News - - * We now have a slack channel, use the button below to join - * Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. - Download for FREE now! -

-[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=latest)](https://dbtvault.readthedocs.io/en/latest/?badge=latest) +[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=stable)](https://dbtvault.readthedocs.io/en/latest/?badge=stable) [![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) @@ -69,12 +63,20 @@ And run var('src_source'), var('source')) }} ``` +## Join our Slack Channel + +Talk to our developers and other members of our growing community, get support and discuss anything related to dbtvault or Data Vault 2.0 + +[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) + ## Sign up for early-bird announcements [![Sign up](https://img.shields.io/badge/Email-Sign--up-blue)](https://www.data-vault.co.uk/dbtvault/) Get notified of new features and new releases before anyone else! +## Starting a Data Vault project + ## Contributing [View our contribution guidelines](CONTRIBUTING.md) diff --git a/dbt_project.yml b/dbt_project.yml index 03ad7784e..a01723977 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -4,7 +4,7 @@ require-dbt-version: [">=0.14.0", "<0.17.0"] profile: 'dbtvault' -source-paths: ["models"] +source-paths: ["models", "models_test"] analysis-paths: ["analysis"] test-paths: ["tests"] data-paths: ["data"] diff --git a/macros/internal/alias.sql b/macros/internal/alias.sql new file mode 100644 index 000000000..0e43c73e2 --- /dev/null +++ b/macros/internal/alias.sql @@ -0,0 +1,53 @@ +{#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro alias(source_column=none, prefix=none) -%} + +{%- if source_column -%} + + {%- if source_column is iterable and source_column is not string -%} + + {%- if source_column['source_column'] and source_column['alias'] -%} + + {%- if prefix -%} + {{prefix}}.{{ source_column['source_column'] }} AS {{ source_column['alias'] }} + {%- else -%} + {{ source_column['source_column'] }} AS {{ source_column['alias'] }} + {%- endif -%} + + {%- endif -%} + + {%- else -%} + + {%- if prefix -%} + + {{- dbtvault.prefix([source_column], prefix) -}} + + {%- else -%} + + {{ source_column }} + + {%- endif -%} + + {%- endif -%} + +{%- else -%} + + {%- if execute -%} + + {{ exceptions.raise_compiler_error("Invalid alias configuration:\nexpected format: {source_column: 'column', alias: 'column_alias'}\ngot: " ~ source_column) }} + + {%- endif -%} + +{%- endif -%} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/single.sql b/macros/internal/alias_all.sql similarity index 69% rename from macros/internal/single.sql rename to macros/internal/alias_all.sql index 95e3cf2c4..6bfd74557 100644 --- a/macros/internal/single.sql +++ b/macros/internal/alias_all.sql @@ -10,11 +10,15 @@ See the License for the specific language governing permissions and limitations under the License. -#} +{%- macro alias_all(columns, prefix) -%} -{%- macro single(src_pk, src_nk, src_ldts, src_source, - source, letter='a') -%} +{%- if columns is iterable and columns is not string -%} - SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], letter) }} - FROM {{ source }} AS {{ letter }} + {%- for column in columns -%} + {{ dbtvault.alias(column, prefix) }} + {%- if not loop.last -%} , {% endif -%} + {%- endfor -%} + +{%- endif -%} {%- endmacro -%} \ No newline at end of file diff --git a/macros/staging/from.sql b/macros/internal/as_constant.sql similarity index 65% rename from macros/staging/from.sql rename to macros/internal/as_constant.sql index 3b9608d79..7a4304d9f 100644 --- a/macros/staging/from.sql +++ b/macros/internal/as_constant.sql @@ -10,9 +10,19 @@ See the License for the specific language governing permissions and limitations under the License. -#} +{%- macro as_constant(column_str) -%} -{% macro from(source_table) %} + {% if column_str is not none %} -FROM {{ source_table }} + {%- if column_str | first == "!" -%} + + {{- return("'" ~ column_str[1:] ~ "'") -}} + + {%- else -%} + + {{- return(column_str) -}} + + {%- endif -%} + {%- endif -%} {%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/docs/internal_macros.md b/macros/internal/docs/internal_macros.md new file mode 100644 index 000000000..6b8bbc6f3 --- /dev/null +++ b/macros/internal/docs/internal_macros.md @@ -0,0 +1,5 @@ +{%- docs macro_alias -%} + +. + +{%- enddocs %} \ No newline at end of file diff --git a/macros/internal/docs/internal_macros_schema.yml b/macros/internal/docs/internal_macros_schema.yml new file mode 100644 index 000000000..fa75207f8 --- /dev/null +++ b/macros/internal/docs/internal_macros_schema.yml @@ -0,0 +1,10 @@ +version: 2 + +macros: + - name: alias + description: "{{ doc('macro_alias') }}" + + arguments: + - name: src_pk + type: string + description: "" \ No newline at end of file diff --git a/macros/internal_deprecated/get_col_list.sql b/macros/internal/expand_column_list.sql similarity index 51% rename from macros/internal_deprecated/get_col_list.sql rename to macros/internal/expand_column_list.sql index c1dc53a04..2420a2f42 100644 --- a/macros/internal_deprecated/get_col_list.sql +++ b/macros/internal/expand_column_list.sql @@ -11,32 +11,41 @@ limitations under the License. -#} -{%- macro get_col_list(tgt_cols) -%} +{%- macro expand_column_list(columns=none) -%} +{%- if not columns -%} + {%- if execute -%} + {{ exceptions.raise_compiler_error("Expected a list of columns, got: " ~ columns) }} + {%- endif -%} +{%- endif -%} {%- set col_list = [] -%} -{%- if tgt_cols is iterable -%} +{%- if columns is iterable -%} - {%- for columns in tgt_cols -%} + {%- for col in columns -%} - {%- if columns is string -%} + {%- if col is string -%} - {%- set _ = col_list.append(columns) -%} + {%- set _ = col_list.append(col) -%} - {#- If a triple -#} - {%- elif columns | first is string -%} + {#- If list of lists -#} + {%- elif col is iterable and col is not string -%} - {%- set _ = col_list.append(columns|last) -%} + {%- if col is mapping -%} - {#- If list of lists -#} - {%- elif columns is iterable and columns is not string -%} + {%- set _ = col_list.append(col) -%} + + {%- else -%} + + {%- for cols in col -%} + + {%- set _ = col_list.append(cols) -%} - {%- for cols in columns -%} + {%- endfor -%} - {%- set _ = col_list.append(cols|last) -%} + {%- endif -%} - {%- endfor -%} {%- endif -%} {%- endfor -%} diff --git a/macros/internal/get_src_col_list.sql b/macros/internal/get_src_col_list.sql deleted file mode 100644 index c38908ecd..000000000 --- a/macros/internal/get_src_col_list.sql +++ /dev/null @@ -1,41 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro get_src_col_list(tgt_cols) -%} - -{%- set col_list = [] -%} - -{%- if tgt_cols is iterable -%} - - {%- for columns in tgt_cols -%} - - {%- if columns is string -%} - - {%- set _ = col_list.append(columns) -%} - - {#- If list of lists -#} - {%- elif columns is iterable and columns is not string -%} - - {%- for cols in columns -%} - - {%- set _ = col_list.append(cols) -%} - - {%- endfor -%} - {%- endif -%} - - {%- endfor -%} -{%- endif -%} - -{{ return(col_list) }} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/hash_check.sql b/macros/internal/hash_check.sql deleted file mode 100644 index 335e82354..000000000 --- a/macros/internal/hash_check.sql +++ /dev/null @@ -1,24 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro hash_check(hash) -%} - -{%- if hash == 'MD5' %} -MD5_BINARY('^^') -{%- elif hash == 'SHA' %} -SHA2_BINARY('^^') -{%- else %} -MD5_BINARY('^^') -{% endif %} - -{% endmacro %} \ No newline at end of file diff --git a/macros/internal/is_multi_source.sql b/macros/internal/is_multi_source.sql deleted file mode 100644 index 0e836af27..000000000 --- a/macros/internal/is_multi_source.sql +++ /dev/null @@ -1,41 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro is_multi_source(source, src_pk, src_fk, src_ldts, src_source) -%} - -{%- if source is iterable and source is not string -%} - {%- set multi_source = [] -%} - {%- for element in source -%} - - {%- set _ = multi_source.append(ref(element)) -%} - - {%- endfor -%} - - {%- set is_union = dbtvault.is_union(multi_source) -%} - {%- set source_col = dbtvault.source_columns(src_pk, src_fk, src_ldts, src_source, - multi_source, is_union) -%} - - {{- return([source_col, is_union]) -}} - -{%- else -%} - - {%- set source = [ref(var('source'))] -%} - {%- set is_union = dbtvault.is_union(source) -%} - {%- set source_col = dbtvault.source_columns(src_pk, src_fk, src_ldts, src_source, - source, is_union) -%} - - {{- return([source_col, is_union]) -}} - -{%- endif -%} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/is_union.sql b/macros/internal/is_union.sql deleted file mode 100644 index cfd004d84..000000000 --- a/macros/internal/is_union.sql +++ /dev/null @@ -1,49 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro is_union(obj) -%} - -{%- if obj is iterable and obj is not string -%} - {%- set checked_relations = [] -%} - {%- for source in obj -%} - {%- set _ = checked_relations.append(dbtvault.check_relation(source)) -%} - {%- endfor -%} - - {#- Not a union if only one source -#} - {%- if checked_relations | length == 1 -%} - - {{- return(false) -}} - - {%- else -%} - {#- Check all are relations -#} - {%- set test_outcome = checked_relations | unique | list -%} - - {%- if test_outcome | length > 1 -%} - - {{- return(false) -}} - - {%- elif test_outcome[0] is sameas true -%} - - {{- return(true) -}} - - {%- else -%} - - {{- return(false) -}} - - {%- endif -%} - - {%- endif -%} - -{%- endif -%} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/multikey.sql b/macros/internal/multikey.sql index 5d181c519..ded071b45 100644 --- a/macros/internal/multikey.sql +++ b/macros/internal/multikey.sql @@ -17,12 +17,12 @@ {% for col in columns if not columns is string %} {% if loop.index == columns|length %} - {{ dbtvault.prefix([col], aliases[0]) }}={{ dbtvault.prefix([col], aliases[1]) }} + {{ dbtvault.prefix([col], aliases[0]) }} = {{ dbtvault.prefix([col], aliases[1]) }} {% else %} - {{ dbtvault.prefix([col], aliases[0]) }}={{ dbtvault.prefix([col], aliases[1]) }} AND + {{ dbtvault.prefix([col], aliases[0]) }} = {{ dbtvault.prefix([col], aliases[1]) }} AND {% endif %} {% else %} - {{ dbtvault.prefix([columns], aliases[0]) }}={{ dbtvault.prefix([columns], aliases[1]) }} + {{ dbtvault.prefix([columns], aliases[0]) }} = {{ dbtvault.prefix([columns], aliases[1]) }} {% endfor %} {% elif type_for == 'where null'%} @@ -41,12 +41,12 @@ {% for col in columns if not columns is string %} {% if loop.index == columns|length %} - {{ dbtvault.prefix([col], aliases[0]) }}<>{{ dbtvault.hash_check(var('hash')) }} - {% else %} - {{ dbtvault.prefix([col], aliases[0]) }}<>{{ dbtvault.hash_check(var('hash')) }} AND - {% endif %} -{% else %} - {{ dbtvault.prefix([columns], aliases[0]) }}<>{{ dbtvault.hash_check(var('hash')) }} + {{ dbtvault.prefix([col], aliases[0]) }} IS NOT NULL + {% else %} + {{ dbtvault.prefix([col], aliases[0]) }} IS NOT NULL AND + {% endif %} +{% else %} + {{ dbtvault.prefix([columns], aliases[0]) }} IS NOT NULL {% endfor %} {% endif %} {% endmacro %} \ No newline at end of file diff --git a/macros/internal/new_union.sql b/macros/internal/new_union.sql deleted file mode 100644 index 8d8fd4144..000000000 --- a/macros/internal/new_union.sql +++ /dev/null @@ -1,36 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro new_union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -%} - - SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'src')}}, - LAG({{ src_source }}, 1) - OVER(PARTITION by {{ tgt_pk }} - ORDER BY {{ tgt_pk }}) AS FIRST_SOURCE - FROM ( - - {%- set letters='abcdefghijklmnopqrstuvwxyz' -%} - - {%- set iterations = source|length -%} - - {%- for src in range(iterations) -%} - {%- set letter = letters[loop.index0] %} - {{ dbtvault.single(src_pk, src_nk, src_ldts, src_source, - source[loop.index0], letter) -}} - - {% if not loop.last %} - UNION - {%- endif -%} - {%- endfor %} - ) AS src -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/retrieve_tgt_cols.sql b/macros/internal/retrieve_tgt_cols.sql deleted file mode 100644 index fa305b769..000000000 --- a/macros/internal/retrieve_tgt_cols.sql +++ /dev/null @@ -1,101 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro retrieve_tgt_cols() -%} - -{%- set tgt_pk = [ ref(kwargs['tgt_pk']|default(None, true)) ] -%} -{%- set tgt_nk = [ ref(kwargs['tgt_nk']|default(None, true))] -%} -{%- set tgt_fk = kwargs['tgt_fk']|default(None, true) -%} -{%- set tgt_payload = kwargs['tgt_payload']|default(None, true) -%} -{%- set tgt_hashdiff = kwargs['tgt_hashdiff']|default(None, true) -%} -{%- set tgt_eff = kwargs['tgt_eff']|default(None, true) -%} -{%- set tgt_ldts = [ ref(kwargs['tgt_ldts']|default(None, true)) ] -%} -{%- set tgt_source = [ ref(kwargs['tgt_source']|default(None, true)) ] -%} - -{%- set src_pk = kwargs['src_pk']|default(None, true) -%} -{%- set src_nk = kwargs['src_nk']|default(None, true) -%} -{%- set src_fk = kwargs['src_fk']|default(None, true) -%} -{%- set src_payload = kwargs['src_payload']|default(None, true) -%} -{%- set src_hashdiff = kwargs['src_hashdiff']|default(None, true) -%} -{%- set src_eff = kwargs['src_eff']|default(None, true) -%} -{%- set src_ldts = kwargs['src_ldts']|default(None, true) -%} -{%- set src_source = kwargs['src_source']|default(None, true) -%} - -{%- set source = kwargs['source']|default(None, true) -%} - -{%- set tgt_cols_dict = {'tgt_pk': (src_pk, tgt_pk, dbtvault.check_relation(tgt_pk[0])), - 'tgt_nk': (src_nk, tgt_nk, dbtvault.check_relation(tgt_nk[0])), - 'tgt_fk': (src_fk, tgt_fk, dbtvault.check_relation(tgt_fk[0])), - 'tgt_payload': (src_payload, tgt_payload, dbtvault.check_relation(tgt_payload[0])), - 'tgt_hashdiff': (src_hashdiff, tgt_hashdiff, dbtvault.check_relation(tgt_hashdiff[0])), - 'tgt_eff': (src_eff, tgt_eff, dbtvault.check_relation(tgt_eff[0])), - 'tgt_ldts': (src_ldts, tgt_ldts, dbtvault.check_relation(tgt_ldts[0])), - 'tgt_source': (src_source, tgt_source, dbtvault.check_relation(tgt_source[0]))} -%} - -{%- set tgt_cols_output = {'tgt_pk': '', - 'tgt_nk': '', - 'tgt_fk': '', - 'tgt_payload': '', - 'tgt_hashdiff': '', - 'tgt_eff': '', - 'tgt_ldts': '', - 'tgt_source': ''} -%} - -{%- set src_cols_list = dbtvault.get_col_list([src_pk, src_nk, src_fk, - src_payload, src_hashdiff, src_eff, - src_ldts, src_source] | reject("none") | list) -%} - -{%- set columns = adapter.get_columns_in_relation(source[0]) -%} -{%- set column_names = columns | map(attribute='name') | list -%} - -{{ dbtvault.validate_columns(src_cols_list, column_names, source[0]) }} - -{%- for col in tgt_cols_dict -%} - - {%- set src_cols = tgt_cols_dict[col][0] -%} - {%- set tgt_col = tgt_cols_dict[col][1] -%} - {%- set is_relation = tgt_cols_dict[col][2] -%} - {%- set tgt_col_list = [] -%} - - {%- if is_relation -%} - - {#- Add column triples to list -#} - {%- if src_cols is iterable and src_cols is not string -%} - {%- for src_col in src_cols -%} - {%- if src_col in column_names -%} - {%- set col_type = columns | selectattr('name', "equalto", src_col) | map(attribute='data_type') | list | default(" ", true) -%} - - {%- set _ = tgt_col_list.append([src_col, col_type[0], src_col]) -%} - {%- endif -%} - {%- endfor -%} - {%- else -%} - {%- set col_type = columns | selectattr('name', "equalto", src_cols) | map(attribute='data_type' ) | list | default(" ", true) -%} - - {%- set _ = tgt_col_list.append([src_cols, col_type[0], src_cols]) -%} - {%- endif -%} - - {%- if tgt_col_list | length > 1 -%} - {%- set _ = tgt_cols_output.update({col: tgt_col_list}) -%} - {%- else -%} - {%- set _ = tgt_cols_output.update({col: tgt_col_list[0]}) -%} - {%- endif -%} - - {%- else -%} - {%- set _ = tgt_cols_output.update({col: tgt_col}) -%} - {%- endif -%} - -{% endfor %} - -{{ return(tgt_cols_output) }} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/source_columns.sql b/macros/internal/source_columns.sql deleted file mode 100644 index f4dc52d6c..000000000 --- a/macros/internal/source_columns.sql +++ /dev/null @@ -1,27 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro source_columns(src_pk, src_nk, src_ldts, src_source, - source, is_union) -%} - - {%- if not is_union -%} - - {{- dbtvault.single(src_pk, src_nk, src_ldts, src_source, source[0], 'a') -}} - - {%- else -%} - - {{- dbtvault.new_union(src_pk, src_nk, src_ldts, src_source, src_pk, source) -}} - - {%- endif -%} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/validate_columns.sql b/macros/internal/validate_columns.sql deleted file mode 100644 index 3d133e71a..000000000 --- a/macros/internal/validate_columns.sql +++ /dev/null @@ -1,24 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro validate_columns(select_columns, source_columns, source_relation) -%} - -{%- if source_columns -%} - {%- for col in select_columns -%} - {%- if col not in source_columns -%} - {{ exceptions.raise_compiler_error("Column '" ~ col ~ "' not present in source '" ~ source_relation.table ~ "', either incorrect source or incorrect source column name.") }} - {%- endif -%} - {%- endfor -%} -{%- endif -%} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal_deprecated/create_source.sql b/macros/internal_deprecated/create_source.sql deleted file mode 100644 index 8014b5681..000000000 --- a/macros/internal_deprecated/create_source.sql +++ /dev/null @@ -1,28 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro create_source(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source, is_union) -%} - - {%- if not is_union -%} - - {{- dbtvault.single(src_pk, src_nk, src_ldts, src_source, source[0], 'a') -}} - - {%- else -%} - - {{- dbtvault.union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -}} - - {%- endif -%} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal_deprecated/create_tgt_cols.sql b/macros/internal_deprecated/create_tgt_cols.sql deleted file mode 100644 index 75358cf0a..000000000 --- a/macros/internal_deprecated/create_tgt_cols.sql +++ /dev/null @@ -1,101 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro create_tgt_cols() -%} - -{%- set tgt_pk = kwargs['tgt_pk']|default(None, true) -%} -{%- set tgt_nk = kwargs['tgt_nk']|default(None, true) -%} -{%- set tgt_fk = kwargs['tgt_fk']|default(None, true) -%} -{%- set tgt_payload = kwargs['tgt_payload']|default(None, true) -%} -{%- set tgt_hashdiff = kwargs['tgt_hashdiff']|default(None, true) -%} -{%- set tgt_eff = kwargs['tgt_eff']|default(None, true) -%} -{%- set tgt_ldts = kwargs['tgt_ldts']|default(None, true) -%} -{%- set tgt_source = kwargs['tgt_source']|default(None, true) -%} - -{%- set src_pk = kwargs['src_pk']|default(None, true) -%} -{%- set src_nk = kwargs['src_nk']|default(None, true) -%} -{%- set src_fk = kwargs['src_fk']|default(None, true) -%} -{%- set src_payload = kwargs['src_payload']|default(None, true) -%} -{%- set src_hashdiff = kwargs['src_hashdiff']|default(None, true) -%} -{%- set src_eff = kwargs['src_eff']|default(None, true) -%} -{%- set src_ldts = kwargs['src_ldts']|default(None, true) -%} -{%- set src_source = kwargs['src_source']|default(None, true) -%} - -{%- set source = kwargs['source']|default(None, true) -%} - -{%- set tgt_cols_dict = {'tgt_pk': (src_pk, tgt_pk, dbtvault.check_relation(tgt_pk[0])), - 'tgt_nk': (src_nk, tgt_nk, dbtvault.check_relation(tgt_nk[0])), - 'tgt_fk': (src_fk, tgt_fk, dbtvault.check_relation(tgt_fk[0])), - 'tgt_payload': (src_payload, tgt_payload, dbtvault.check_relation(tgt_payload[0])), - 'tgt_hashdiff': (src_hashdiff, tgt_hashdiff, dbtvault.check_relation(tgt_hashdiff[0])), - 'tgt_eff': (src_eff, tgt_eff, dbtvault.check_relation(tgt_eff[0])), - 'tgt_ldts': (src_ldts, tgt_ldts, dbtvault.check_relation(tgt_ldts[0])), - 'tgt_source': (src_source, tgt_source, dbtvault.check_relation(tgt_source[0]))} -%} - -{%- set tgt_cols_output = {'tgt_pk': '', - 'tgt_nk': '', - 'tgt_fk': '', - 'tgt_payload': '', - 'tgt_hashdiff': '', - 'tgt_eff': '', - 'tgt_ldts': '', - 'tgt_source': ''} -%} - -{%- set src_cols_list = dbtvault.get_col_list([src_pk, src_nk, src_fk, - src_payload, src_hashdiff, src_eff, - src_ldts, src_source] | reject("none") | list) -%} - -{%- set columns = adapter.get_columns_in_relation(source[0]) -%} -{%- set column_names = columns | map(attribute='name') | list -%} - -{{ dbtvault.validate_columns(src_cols_list, column_names, source[0]) }} - -{%- for col in tgt_cols_dict -%} - - {%- set src_cols = tgt_cols_dict[col][0] -%} - {%- set tgt_col = tgt_cols_dict[col][1] -%} - {%- set is_relation = tgt_cols_dict[col][2] -%} - {%- set tgt_col_list = [] -%} - - {%- if is_relation -%} - - {#- Add column triples to list -#} - {%- if src_cols is iterable and src_cols is not string -%} - {%- for src_col in src_cols -%} - {%- if src_col in column_names -%} - {%- set col_type = columns | selectattr('name', "equalto", src_col) | map(attribute='data_type') | list | default(" ", true) -%} - - {%- set _ = tgt_col_list.append([src_col, col_type[0], src_col]) -%} - {%- endif -%} - {%- endfor -%} - {%- else -%} - {%- set col_type = columns | selectattr('name', "equalto", src_cols) | map(attribute='data_type' ) | list | default(" ", true) -%} - - {%- set _ = tgt_col_list.append([src_cols, col_type[0], src_cols]) -%} - {%- endif -%} - - {%- if tgt_col_list | length > 1 -%} - {%- set _ = tgt_cols_output.update({col: tgt_col_list}) -%} - {%- else -%} - {%- set _ = tgt_cols_output.update({col: tgt_col_list[0]}) -%} - {%- endif -%} - - {%- else -%} - {%- set _ = tgt_cols_output.update({col: tgt_col}) -%} - {%- endif -%} - -{% endfor %} - -{{ return(tgt_cols_output) }} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal_deprecated/union.sql b/macros/internal_deprecated/union.sql deleted file mode 100644 index 45f1290b4..000000000 --- a/macros/internal_deprecated/union.sql +++ /dev/null @@ -1,35 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro union(src_pk, src_nk, src_ldts, src_source, tgt_pk, source) -%} - - SELECT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'src')}}, - LAG({{ src_source }}, 1) - OVER(PARTITION by {{ tgt_pk | last }} - ORDER BY {{ tgt_pk | last }}) AS FIRST_SOURCE - FROM ( - - {%- set letters='abcdefghijklmnopqrstuvwxyz' -%} - - {%- set iterations = source|length -%} - - {%- for src in range(iterations) -%} - {%- set letter = letters[loop.index0] %} - {{ dbtvault.single(src_pk, src_nk, src_ldts, src_source, - source[loop.index0], letter) -}} - {% if not loop.last %} - UNION - {%- endif -%} - {%- endfor %} - ) AS src -{%- endmacro -%} \ No newline at end of file diff --git a/macros/staging/add_columns.sql b/macros/staging/add_columns.sql deleted file mode 100644 index 14593889d..000000000 --- a/macros/staging/add_columns.sql +++ /dev/null @@ -1,49 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro add_columns(source, pairs=[]) -%} - -{%- set exclude_columns = [] -%} -{%- set include_columns = [] -%} - -{%- if source is defined and source is not none -%} -{%- set cols = adapter.get_columns_in_relation(source) -%} -{%- endif %} - -{#- Add aliases of provided pairs to excludes and full SQL to includes -#} -{%- for pair in pairs -%} - {%- if pair[0] | first == "!" -%} - {%- set _ = include_columns.append("'" ~ pair[0][1:] ~ "' AS " ~ pair[1]) -%} - {%- set _ = exclude_columns.append(pair[1]) -%} - {%- else -%} - {%- set _ = include_columns.append(pair[0] ~ " AS " ~ pair[1]) -%} - {%- set _ = exclude_columns.append(pair[1]) -%} - {%- endif %} -{%- endfor -%} - -{%- if source is defined and source is not none -%} -{#- Add all columns from source table -#} -{%- for col in cols -%} - {%- if col.column not in exclude_columns -%} - {%- set _ = include_columns.append(col.column) -%} - {%- endif -%} -{%- endfor -%} -{%- endif %} - -{#- Print out all columns in includes -#} -{%- for col in include_columns %} - {{ col }}{%if not loop.last %}, -{%- endif -%} - -{%- endfor -%} -{%- endmacro -%} \ No newline at end of file diff --git a/macros/staging/derive_columns.sql b/macros/staging/derive_columns.sql new file mode 100644 index 000000000..9edc2f315 --- /dev/null +++ b/macros/staging/derive_columns.sql @@ -0,0 +1,79 @@ +{#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} + +{%- macro derive_columns(source_relation=none, columns=none) -%} + +{%- set exclude_columns = [] -%} +{%- set include_columns = [] -%} + +{%- if source_relation is defined and source_relation is not none -%} + {%- set source_model_cols = adapter.get_columns_in_relation(source_relation) -%} +{%- endif %} + +{%- if columns is mapping and columns is not none -%} + + {#- Add aliases of provided columns to excludes and full SQL to includes -#} + {%- for col in columns -%} + + {% set column_str = dbtvault.as_constant(columns[col]) %} + + {%- set _ = include_columns.append(column_str ~ " AS " ~ col) -%} + {%- set _ = exclude_columns.append(col) -%} + + {%- endfor -%} + + {#- Add all columns from source_model relation -#} + {%- if source_relation is defined and source_relation is not none -%} + + {%- for source_col in source_model_cols -%} + {%- if source_col.column not in exclude_columns -%} + {%- set _ = include_columns.append(source_col.column) -%} + {%- endif -%} + {%- endfor -%} + + {%- endif %} + + {#- Print out all columns in includes -#} + {%- for col in include_columns -%} + {{ col }} + {%- if not loop.last -%}, +{% endif -%} + {%- endfor -%} + +{%- elif columns is none and source_relation is not none -%} + + {#- Add all columns from source_model relation -#} + {%- for source_col in source_model_cols -%} + {%- if source_col.column not in exclude_columns -%} + {%- set _ = include_columns.append(source_col.column) -%} + {%- endif -%} + {%- endfor -%} + + {#- Print out all columns in includes -#} + {%- for col in include_columns -%} + {{ col }} + {{- ',\n' if not loop.last -}} + + {%- endfor -%} + +{%- else -%} + +{%- if execute -%} +{{ exceptions.raise_compiler_error("Invalid column configuration: +expected format: {source_relation: Relation, columns: 'column_mapping'} +got: {'source_relation': " ~ source_relation ~ ", 'columns': " ~ columns ~ "}") }} +{%- endif %} + +{%- endif %} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/staging/hash_columns.sql b/macros/staging/hash_columns.sql new file mode 100644 index 000000000..46fd5a45f --- /dev/null +++ b/macros/staging/hash_columns.sql @@ -0,0 +1,43 @@ +{#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} + +{%- macro hash_columns(columns=none) -%} + +{%- if columns is mapping -%} + + {%- for col in columns -%} + + {% if columns[col] is mapping and columns[col].hashdiff -%} + + {{- dbtvault.hash(columns[col]['columns'], col, columns[col]['hashdiff']) -}} + + {%- elif columns[col] is not mapping -%} + + {{- dbtvault.hash(columns[col], col, hashdiff=false) -}} + + {%- elif columns[col] is mapping and not columns[col].hashdiff -%} + + {%- if execute -%} + {%- do exceptions.warn("[" ~ this ~ "] Warning: You provided a list of columns under a 'columns' key, but did not provide the 'hashdiff' flag. Use list syntax for PKs.") -%} + {% endif %} + + {{- dbtvault.hash(columns[col]['columns'], col) -}} + + {%- endif -%} + + {%- if not loop.last -%}, +{% endif %} + {%- endfor -%} + +{%- endif -%} +{%- endmacro -%} diff --git a/macros/staging/multi_hash.sql b/macros/staging/multi_hash.sql deleted file mode 100644 index 06c8ce17b..000000000 --- a/macros/staging/multi_hash.sql +++ /dev/null @@ -1,30 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro multi_hash(triples) -%} --- Generated by dbtvault. -SELECT -{% for triple in triples -%} - {%- if triple | length == 2 -%} - - {{ dbtvault.hash(triple[0], triple[1]) }} - - {%- elif triple | length == 3 and triple | last == true -%} - - {{ dbtvault.hash(triple[0], triple[1], triple[2]) }} - - {%- endif -%} - - {% if not loop.last -%}, {% endif %} -{%- endfor -%} -{%- endmacro -%} diff --git a/macros/staging/stage.sql b/macros/staging/stage.sql new file mode 100644 index 000000000..4e049bc0b --- /dev/null +++ b/macros/staging/stage.sql @@ -0,0 +1,80 @@ + {#- Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-#} +{%- macro stage(include_source_columns=none, source_model=none, hashed_columns=none, derived_columns=none) -%} + + {% if include_source_columns is none %} + {%- set include_source_columns = true -%} + {% endif %} + + {{- adapter_macro('dbtvault.stage', include_source_columns=include_source_columns, source_model=source_model, hashed_columns=hashed_columns, derived_columns=derived_columns) -}} +{%- endmacro -%} + +{%- macro default__stage(include_source_columns, source_model, hashed_columns, derived_columns) -%} +-- Generated by dbtvault. + +{% if (source_model is none) and execute %} + + {%- set error_message -%} + "Staging error: Missing source_model configuration. A source model name must be provided. + e.g. + [REF STYLE] + source_model: model_name + OR + [SOURCES STYLE] + source_model: + source_name: source_table_name" + {%- endset -%} + + {{- exceptions.raise_compiler_error(error_message) -}} +{%- endif -%} + +SELECT + +{# Create relation object from provided source_model -#} +{% if source_model is mapping and source_model is not none -%} + + {%- set source_name = source_model | first -%} + {%- set source_table_name = source_model[source_name] -%} + + {%- set source_relation = source(source_name, source_table_name) -%} + +{%- elif source_model is not mapping and source_model is not none -%} + + {%- set source_relation = ref(source_model) -%} +{%- endif -%} + +{#- Hash columns, if provided -#} +{% if hashed_columns is defined and hashed_columns is not none -%} + + {{ dbtvault.hash_columns(columns=hashed_columns) -}} + {{ "," if derived_columns is defined and source_relation is defined and include_source_columns }} + +{% endif -%} + +{#- Derive additional columns, if provided -#} +{%- if derived_columns is defined and derived_columns is not none -%} + + {%- if include_source_columns -%} + {{ dbtvault.derive_columns(source_relation=source_relation, columns=derived_columns) }} + {%- else -%} + {{ dbtvault.derive_columns(columns=derived_columns) }} + {%- endif -%} +{#- If source relation is defined but derived_columns is not, add columns from source model. -#} +{%- elif source_relation is defined and include_source_columns is true -%} + + {{ dbtvault.derive_columns(source_relation=source_relation) }} +{%- endif %} + +FROM {{ source_relation }} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/supporting/cast.sql b/macros/supporting/cast.sql index 9e3e02abd..4fbbba041 100644 --- a/macros/supporting/cast.sql +++ b/macros/supporting/cast.sql @@ -19,7 +19,7 @@ {#- If only single string provided -#} {%- if columns is string -%} - {{columns}} + {{- columns -}} {%- else -%} @@ -27,22 +27,22 @@ {#- Output String if just a string -#} {%- if column is string -%} - {% if prefix %} - {{ dbtvault.prefix([column], prefix) }} - {%- else %} - {{ column }} + {%- if prefix -%} + {{- dbtvault.prefix([column], prefix) -}} + {%- else -%} + {{- column -}} {%- endif -%} {#- Recurse if a list of lists (i.e. multi-column key) -#} {%- elif column|first is iterable and column|first is not string -%} - {{ dbtvault.cast(column, prefix) }} + {{- dbtvault.cast(column, prefix) -}} {#- Otherwise it is a standard list -#} {%- else -%} {#- Make sure it is a triple -#} - {%- if column|length == 3 %} - {% if prefix -%} + {%- if column|length == 3 -%} + {%- if prefix -%} CAST({{ dbtvault.prefix([column[0]], prefix) }} AS {{ column[1] }}) AS {{ column[2] }} {%- else -%} CAST({{ column[0] }} AS {{ column[1] }}) AS {{ column[2] }} @@ -52,7 +52,7 @@ {%- endif -%} {#- Add trailing comma if not last -#} - {%- if not loop.last -%} , {%- endif -%} + {{ ',\n' if not loop.last }} {%- endfor -%} diff --git a/macros/supporting/hash.sql b/macros/supporting/hash.sql index a60239df5..290070046 100644 --- a/macros/supporting/hash.sql +++ b/macros/supporting/hash.sql @@ -1,17 +1,17 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); +{# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --#} +#} -{%- macro hash(columns, alias, sort=false) -%} +{%- macro hash(columns=none, alias=none, hashdiff=false) -%} {%- set hash = var('hash', 'MD5') -%} @@ -27,24 +27,34 @@ {%- set hash_size = 16 -%} {%- endif -%} -{#- Alpha sort columns before hashing -#} -{%- if sort and columns is iterable and columns is not string -%} -{%- set columns = columns|sort -%} +{#- Alpha sort columns before hashing if a hashdiff -#} +{%- if hashdiff and columns is iterable and columns is not string -%} + {%- set columns = columns|sort -%} {%- endif -%} -{%- if columns is string %} - CAST({{- hash_alg -}}(IFNULL((UPPER(TRIM(CAST({{columns}} AS VARCHAR)))), '^^')) AS BINARY({{- hash_size -}})) AS {{alias}} +{#- If single column to hash -#} +{%- if columns is string -%} + {%- set column_str = dbtvault.as_constant(columns) -%} + CAST(({{ hash_alg }}(NULLIF(UPPER(TRIM(CAST({{ column_str }} AS VARCHAR))), ''))) AS BINARY({{ hash_size }})) AS {{ alias }} +{#- Else a list of columns to hash -#} +{%- else -%} + +CAST({{ hash_alg }}(CONCAT( + +{%- for column in columns %} + +{%- set column_str = dbtvault.as_constant(column) -%} + +{%- if not loop.last %} + IFNULL(NULLIF(UPPER(TRIM(CAST({{ column_str }} AS VARCHAR))), ''), '^^'), '||', {%- else %} + IFNULL(NULLIF(UPPER(TRIM(CAST({{ column_str }} AS VARCHAR))), ''), '^^') )) +AS BINARY({{ hash_size }})) AS {{ alias }} +{%- endif -%} - CAST({{- hash_alg -}}(CONCAT( -{%- for column in columns[:-1] %} - IFNULL(UPPER(TRIM(CAST({{- column }} AS VARCHAR))), '^^'), '||', +{%- endfor -%} +{%- endif -%} -{%- if loop.last %} - IFNULL(UPPER(TRIM(CAST({{columns[-1]}} AS VARCHAR))), '^^') )) AS BINARY({{- hash_size -}})) AS {{alias}} -{%- endif -%} -{%- endfor -%} -{%- endif -%} {%- endmacro -%} diff --git a/macros/supporting/prefix.sql b/macros/supporting/prefix.sql index d8e5d3ee2..fe081258e 100644 --- a/macros/supporting/prefix.sql +++ b/macros/supporting/prefix.sql @@ -11,18 +11,56 @@ limitations under the License. -#} -{%- macro prefix(columns, prefix_str) -%} +{%- macro prefix(columns=none, prefix_str=none, alias_target='source') -%} -{%- for column in columns -%} + {%- if columns and prefix_str -%} - {% if column is iterable and column is not string %} - {{- dbtvault.prefix(column, prefix_str) -}} - {%- else -%} - {{- prefix_str}}.{{column.strip() -}} - {%- endif -%} + {%- for col in columns -%} + + {%- if col is mapping -%} + + {%- if alias_target == 'source' -%} + + {{- dbtvault.prefix([col['source_column']], prefix_str) -}} + + {%- elif alias_target == 'target' -%} + + {{- dbtvault.prefix([col['alias']], prefix_str) -}} + + {%- else -%} + + {{- dbtvault.prefix([col['source_column']], prefix_str) -}} + + {%- endif -%} + + {%- if not loop.last -%} , {% endif %} - {%- if not loop.last -%} , {% endif %} + {%- else -%} -{%- endfor -%} + {%- if col is iterable and col is not string -%} + + {{- dbtvault.prefix(col, prefix_str) -}} + + {%- elif col is not none -%} + {{- prefix_str}}.{{col.strip() -}} + {% else %} + + {%- if execute -%} + {{- exceptions.raise_compiler_error("Unexpected or missing configuration for '" ~ this ~ "' Unable to prefix columns.") -}} + {%- endif -%} + {%- endif -%} + + {{- ', ' if not loop.last -}} + + {%- endif -%} + + {%- endfor -%} + + {%- else -%} + + {%- if execute -%} + {{- exceptions.raise_compiler_error("Invalid parameters provided to prefix macro. Expected: (columns [list/string], prefix_str [string]) got: (" ~ columns ~ ", " ~ prefix_str ~ ")") -}} + {%- endif -%} + {%- endif -%} {%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/eff_sat.sql b/macros/tables/eff_sat.sql deleted file mode 100644 index eb4ab9d0f..000000000 --- a/macros/tables/eff_sat.sql +++ /dev/null @@ -1,114 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro eff_sat(src_pk, src_dfk, src_sfk, src_ldts, src_eff_from, src_start_date, src_end_date, src_source, link, source)-%} - -{%- set source_cols = dbtvault.get_src_col_list([src_pk, src_ldts, src_eff_from, src_start_date, src_end_date, src_source])-%} -{%- set max_date = "'" ~ '9999-12-31' ~ "'" -%} --- Generated by dbtvault. -{% if is_incremental() %} -WITH -{#- Reduce data set to size of stage table. #} -c AS (SELECT DISTINCT - {{ dbtvault.prefix(source_cols, 'a') }} - FROM {{ this }} AS a - INNER JOIN {{ ref(source) }} AS b ON {{ dbtvault.prefix([src_pk], 'a') }}={{ dbtvault.prefix([src_pk], 'b') }} - ) -{# Find latest satellite for each pk in set c. -#} -, d as (SELECT - {{ dbtvault.prefix(source_cols, 'c') }}, - CASE WHEN RANK() - OVER (PARTITION BY {{ dbtvault.prefix([src_pk], 'c') }} - ORDER BY {{ dbtvault.prefix([src_end_date], 'c') }} ASC) = 1 - THEN 'Y' ELSE 'N' END AS CURR_FLG - FROM c) -, p AS ( - SELECT q.* FROM {{ ref(link) }} AS q - INNER JOIN {{ ref(source) }} AS r ON - {{ dbtvault.multikey(src_dfk, ['q', 'r'], 'join') }} -) -, x AS ( - SELECT p.* - {% for dfk in src_dfk if not src_dfk is string %} - , {{ dbtvault.prefix([dfk], 's') }} AS DFK_{{ loop.index }} - {% else %} - , {{ dbtvault.prefix([src_dfk], 's') }} AS DFK_1 - {% endfor %} - FROM p - LEFT JOIN {{ ref(source) }} AS s ON - {{ dbtvault.multikey(src_dfk, ['p', 's'], 'join') }} - AND - {{ dbtvault.multikey(src_sfk, ['p', 's'], 'join') }} - WHERE ( - {{ dbtvault.multikey(src_dfk, ['s'], 'where null') }} - AND - {{ dbtvault.multikey(src_sfk, ['s'], 'where null') }} - ) -) -, y AS ( - SELECT - {{ dbtvault.prefix([src_pk, src_ldts, src_source, src_eff_from, src_start_date, src_end_date], 't') }} - {% for dfk in src_dfk if not src_dfk is string %} - , {{ dbtvault.prefix(['DFK_'~loop.index ], 'x') }} - {% else %} - , {{ dbtvault.prefix(['DFK_1'], 'x') }} - {% endfor %} - , {{ dbtvault.prefix([src_dfk], 'x')}}, - CASE WHEN RANK() - OVER (PARTITION BY {{ dbtvault.prefix([src_pk], 't') }} - ORDER BY {{ dbtvault.prefix([src_end_date], 't') }} ASC) = 1 - THEN 'Y' ELSE 'N' END AS CURR_FLG - FROM x - INNER JOIN {{ this }} AS t ON {{ dbtvault.prefix([src_pk], 'x') }}={{ dbtvault.prefix([src_pk], 't') }} - ) -{% endif %} -SELECT DISTINCT - {{ dbtvault.prefix([src_pk, src_ldts, src_source, src_eff_from], 'e') }}, - {{ dbtvault.prefix([src_eff_from], 'e') }} AS {{ src_start_date }}, - {{ dbtvault.prefix([src_end_date], 'e') }} -FROM {{ ref(source) }} AS e -{% if is_incremental() -%} -LEFT JOIN ( - SELECT {{ dbtvault.prefix(source_cols, 'd')}} - FROM d - WHERE d.CURR_FLG = 'Y' AND {{ dbtvault.prefix([src_end_date], 'd') }}=TO_DATE({{ max_date }}) - ) AS eff -ON {{ dbtvault.prefix([src_pk], 'eff') }}={{ dbtvault.prefix([src_pk], 'e') }} -WHERE ({{ dbtvault.prefix([src_pk], 'eff') }} IS NULL -AND -{{ dbtvault.multikey(src_sfk, ['e'], 'where not null') }} -AND -{{ dbtvault.multikey(src_dfk, ['e'], 'where not null') }} -) -UNION -SELECT - {{ dbtvault.prefix([src_pk], 'y') }}, - {{ dbtvault.prefix([src_ldts], 'z') }}, - {{ dbtvault.prefix([src_source, src_eff_from, src_start_date], 'y') }}, - CASE WHEN - {% for dfk in src_dfk if not src_dfk is string %} - {% if loop.index == src_dfk|length %} - y.DFK_{{loop.index|string}} IS NULL - {% else %} - y.DFK_{{loop.index|string}} IS NULL AND - {% endif %} - {% else %} - y.DFK_1 IS NULL - {% endfor %} - THEN {{ dbtvault.prefix([src_eff_from], 'z') }} ELSE {{ max_date }} END AS {{ src_end_date }} -FROM y -LEFT JOIN {{ ref(source) }} AS z ON -{{ dbtvault.multikey(src_dfk, ['y', 'z'], 'join') }} -WHERE (y.CURR_FLG='Y' AND {{ dbtvault.prefix([src_end_date], 'y') }}={{ max_date }}) -{%- endif -%} -{% endmacro %} \ No newline at end of file diff --git a/macros/tables/hub.sql b/macros/tables/hub.sql index 634e10bcf..33b78ded4 100644 --- a/macros/tables/hub.sql +++ b/macros/tables/hub.sql @@ -11,30 +11,75 @@ limitations under the License. -#} -{%- macro hub(src_pk, src_nk, src_ldts, src_source, - source) -%} +{%- macro hub(src_pk, src_nk, src_ldts, src_source, source_model) -%} -{%- set source_data = dbtvault.is_multi_source(source, src_pk, src_nk, src_ldts, src_source) -%} -{%- set source_col = source_data[0] -%} -{%- set is_union = source_data[1] -%} +{%- set source_cols = dbtvault.expand_column_list([src_pk, src_nk, src_ldts, src_source]) -%} -- Generated by dbtvault. -SELECT DISTINCT {{ dbtvault.prefix([src_pk, src_nk, src_ldts, src_source], 'stg') }} -FROM ( - {{ source_col }} -) AS stg -{# If incremental union or single #} -{%- if is_incremental() -%} -LEFT JOIN {{ this }} AS tgt -ON {{ dbtvault.prefix([src_pk], 'stg') }} = {{ dbtvault.prefix([src_pk], 'tgt') }} -WHERE {{ dbtvault.prefix([src_pk], 'tgt') }} IS NULL -{# If an incremental and union load -#} -{% if is_union -%} -AND stg.FIRST_SOURCE IS NULL -{%- endif -%} -{%- endif -%} -{# If a union base-load #} -{%- if is_union and not is_incremental() -%} -WHERE stg.FIRST_SOURCE IS NULL +{{ 'WITH ' -}} + +{%- if source_model is iterable and source_model is not string -%} + +{%- for src in source_model -%} + +STG_{{ loop.index|string }} AS ( + SELECT DISTINCT + {{ dbtvault.prefix(source_cols, 'a') }} + FROM ( + SELECT {{ src_pk }}, {{ src_nk }}, {{ src_ldts }}, {{ src_source }}, + ROW_NUMBER() OVER( + PARTITION BY {{ src_pk }} + ORDER BY {{ src_ldts }} ASC + ) AS RN + FROM {{ ref(src) }} + ) AS a + WHERE RN = 1 +), +{% endfor -%} +STG AS ( + SELECT DISTINCT + {{ dbtvault.prefix(source_cols, 'b') }} + FROM ( + SELECT *, + ROW_NUMBER() OVER( + PARTITION BY {{ src_pk }} + ORDER BY {{ src_ldts }}, {{ src_source }} ASC + ) AS RN + FROM ( + {%- for src in source_model %} + SELECT * {{ 'FROM ' -}} STG_{{ loop.index|string }} + {%- if not loop.last %} + UNION ALL + {%- endif %} + {%- endfor %} + ) + WHERE {{ src_pk }} IS NOT NULL + ) AS b + WHERE RN = 1 +) +{%- else -%} + +STG AS ( + SELECT DISTINCT + {{ dbtvault.prefix(source_cols, 'a') }} + FROM ( + SELECT b.*, + ROW_NUMBER() OVER( + PARTITION BY {{ dbtvault.prefix([src_pk], 'b') }} + ORDER BY {{ dbtvault.prefix([src_ldts], 'b') }}, {{ dbtvault.prefix([src_source], 'b') }} ASC + ) AS RN + FROM {{ ref(source_model) }} AS b + WHERE {{ dbtvault.prefix([src_pk], 'b') }} IS NOT NULL + ) AS a + WHERE RN = 1 +) +{%- endif %} + +SELECT c.* FROM STG AS c +{%- if is_incremental() %} +LEFT JOIN {{ this }} AS d +ON {{ dbtvault.prefix([src_pk], 'c') }} = {{ dbtvault.prefix([src_pk], 'd') }} +WHERE {{ dbtvault.prefix([src_pk], 'd') }} IS NULL {%- endif -%} + {%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/link.sql b/macros/tables/link.sql index 357129aba..9270a9999 100644 --- a/macros/tables/link.sql +++ b/macros/tables/link.sql @@ -11,44 +11,94 @@ limitations under the License. -#} -{%- macro link(src_pk, src_fk, src_ldts, src_source, - source) -%} +{%- macro link(src_pk, src_fk, src_ldts, src_source, source_model) -%} -{%- set source_data = dbtvault.is_multi_source(source, src_pk, src_fk, src_ldts, src_source) -%} -{%- set source_col = source_data[0] -%} -{%- set is_union = source_data[1] -%} +{%- set source_cols = dbtvault.expand_column_list([src_pk, src_fk, src_ldts, src_source]) -%} +{%- set fk_cols = dbtvault.expand_column_list([src_fk]) -%} -- Generated by dbtvault. -SELECT DISTINCT {{ dbtvault.prefix([src_pk, src_fk, src_ldts, src_source], 'stg') }} -FROM ( - {{ source_col }} -) AS stg -{# If incremental union or single #} -{%- if is_incremental() -%} -LEFT JOIN {{ this }} AS tgt -ON {{ dbtvault.prefix([src_pk], 'stg') }} = {{ dbtvault.prefix([src_pk], 'tgt') }} -WHERE {{ dbtvault.prefix([src_pk], 'tgt') }} IS NULL -{% if is_union -%} -AND stg.FIRST_SOURCE IS NULL -{%- endif -%} -{%- for fk in src_fk %} -AND {{ dbtvault.prefix([fk], 'stg') }}<>{{ dbtvault.hash_check(var('hash')) }} -{% endfor %} -{%- elif not is_incremental() -%} -{% if is_union %} -WHERE stg.FIRST_SOURCE IS NULL -{%- for fk in src_fk %} -AND {{ dbtvault.prefix([fk], 'stg') }}<>{{ dbtvault.hash_check(var('hash')) }} -{% endfor %} -{% else %} -WHERE -{%- for fk in src_fk %} -{% if loop.index == src_fk|length %} -{{ dbtvault.prefix([fk], 'stg') }}<>{{ dbtvault.hash_check(var('hash')) }} -{% else %} -{{ dbtvault.prefix([fk], 'stg') }}<>{{ dbtvault.hash_check(var('hash')) }} AND -{% endif %} -{% endfor %} -{% endif %} +{{ 'WITH ' -}} + +{%- if source_model is iterable and source_model is not string -%} + +{%- for src in source_model -%} + +STG_{{ loop.index|string }} AS ( + SELECT DISTINCT + {{ dbtvault.prefix(source_cols, 'a') }} + FROM ( + SELECT {{ src_pk }} + {%- for fk in fk_cols -%} + , {{ fk }} + {%- endfor -%} + , {{ src_ldts }}, {{ src_source }}, + ROW_NUMBER() OVER( + PARTITION BY {{ src_pk }} + ORDER BY {{ src_ldts }} ASC + ) AS RN + FROM {{ ref(src) }} + ) AS a + WHERE RN = 1 +), +{% endfor -%} + +STG AS ( + SELECT DISTINCT + {{ dbtvault.prefix(source_cols, 'b') }} + FROM ( + SELECT *, + ROW_NUMBER() OVER( + PARTITION BY {{ src_pk }} + ORDER BY {{ src_ldts }}, {{ src_source }} ASC + ) AS RN + FROM ( + {%- for src in source_model %} + SELECT * FROM STG_{{ loop.index|string }} + {%- if not loop.last %} + UNION ALL + {%- endif %} + {%- endfor %} + ) + {{ 'WHERE' -}} + {%- for fk in fk_cols -%} + {%- if not loop.last %} + {{ fk }} IS NOT NULL AND + {%- else %} + {{ fk }} IS NOT NULL + {%- endif -%} + {%- endfor %} + ) AS b + WHERE RN = 1 +) +{%- else -%} +STG AS ( + SELECT DISTINCT + {{ dbtvault.prefix(source_cols, 'a') }} + FROM ( + SELECT b.*, + ROW_NUMBER() OVER( + PARTITION BY {{ dbtvault.prefix([src_pk], 'b') }} + ORDER BY b.{{ src_ldts }}, b.{{ src_source }} ASC + ) AS RN + FROM {{ ref(source_model) }} AS b + {{ 'WHERE' -}} + {%- for fk in fk_cols -%} + {%- if not loop.last %} + b.{{ fk }} IS NOT NULL AND + {%- else %} + b.{{ fk }} IS NOT NULL + {%- endif -%} + {%- endfor %} + ) AS a + WHERE RN = 1 +) +{%- endif %} + +SELECT c.* FROM STG AS c +{%- if is_incremental() %} +LEFT JOIN {{ this }} AS d +ON {{ dbtvault.prefix([src_pk], 'c') }} = {{ dbtvault.prefix([src_pk], 'd') }} +WHERE {{ dbtvault.prefix([src_pk], 'd') }} IS NULL {%- endif -%} + {%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/sat.sql b/macros/tables/sat.sql index fc6201fa4..3e97103e8 100644 --- a/macros/tables/sat.sql +++ b/macros/tables/sat.sql @@ -11,34 +11,35 @@ limitations under the License. -#} -{%- macro sat(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - source) -%} +{%- macro sat(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, source_model) -%} -{%- set source_cols = dbtvault.get_src_col_list([src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source]) -%} +{%- set source_cols = dbtvault.expand_column_list([src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source]) -%} -- Generated by dbtvault. -SELECT DISTINCT {{ dbtvault.prefix(source_cols, 'e') }} -FROM {{ ref(source) }} AS e -{% if is_incremental() -%} +{% if not is_incremental() -%} +SELECT DISTINCT {{ dbtvault.alias_all(source_cols, 'e') }} +FROM {{ ref(source_model) }} AS e +{% else -%} +SELECT DISTINCT {{ dbtvault.alias_all(source_cols, 'e') }} +FROM {{ ref(source_model) }} AS e LEFT JOIN ( - SELECT {{ dbtvault.prefix(source_cols, 'd') }} + SELECT {{ dbtvault.prefix(source_cols, 'd', alias_target='target') }} FROM ( - SELECT {{ dbtvault.prefix(source_cols, 'c') }}, + SELECT {{ dbtvault.prefix(source_cols, 'c', alias_target='target') }}, CASE WHEN RANK() OVER (PARTITION BY {{ dbtvault.prefix([src_pk], 'c') }} ORDER BY {{ dbtvault.prefix([src_ldts], 'c') }} DESC) = 1 THEN 'Y' ELSE 'N' END CURR_FLG FROM ( - SELECT {{ dbtvault.prefix(source_cols, 'a') }} + SELECT {{ dbtvault.prefix(source_cols, 'a', alias_target='target') }} FROM {{ this }} as a - JOIN {{ ref(source) }} as b + JOIN {{ ref(source_model) }} as b ON {{ dbtvault.prefix([src_pk], 'a') }} = {{ dbtvault.prefix([src_pk], 'b') }} ) as c ) AS d WHERE d.CURR_FLG = 'Y') AS src -ON {{ dbtvault.prefix([src_hashdiff], 'src') }} = {{ dbtvault.prefix([src_hashdiff], 'e') }} -WHERE {{ dbtvault.prefix([src_hashdiff], 'src') }} IS NULL +ON {{ dbtvault.prefix([src_hashdiff], 'src', alias_target='target') }} = {{ dbtvault.prefix([src_hashdiff], 'e') }} +WHERE {{ dbtvault.prefix([src_hashdiff], 'src', alias_target='target') }} IS NULL {%- endif -%} {% endmacro %} diff --git a/macros/tables/t_link.sql b/macros/tables/t_link.sql index 8833a414c..5b9fac9b9 100644 --- a/macros/tables/t_link.sql +++ b/macros/tables/t_link.sql @@ -11,15 +11,15 @@ limitations under the License. -#} -{%- macro t_link(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, source) -%} +{%- macro t_link(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, source_model) -%} -{%- set source_cols = dbtvault.get_src_col_list([src_pk, src_fk, src_payload, src_eff, src_ldts, src_source])-%} +{%- set source_cols = dbtvault.expand_column_list([src_pk, src_fk, src_payload, src_eff, src_ldts, src_source])-%} -- Generated by dbtvault. SELECT DISTINCT {{ dbtvault.prefix(source_cols, 'stg') }} FROM ( SELECT {{ dbtvault.prefix(source_cols, 'stg') }} - FROM {{ ref(source) }} AS stg + FROM {{ ref(source_model) }} AS stg ) AS stg {% if is_incremental() -%} LEFT JOIN {{ this }} AS tgt diff --git a/macros/tables_deprecated/hub_template.sql b/macros/tables_deprecated/hub_template.sql deleted file mode 100644 index e7a9b7060..000000000 --- a/macros/tables_deprecated/hub_template.sql +++ /dev/null @@ -1,49 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro hub_template(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source) -%} - -{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_nk=src_nk, src_ldts=src_ldts, src_source=src_source, - tgt_pk=tgt_pk, tgt_nk=tgt_nk, tgt_ldts=tgt_ldts, tgt_source=tgt_source, - source=source) -%} - -{%- set tgt_pk = tgt_cols['tgt_pk'] -%} -{%- set tgt_nk = tgt_cols['tgt_nk'] -%} -{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} -{%- set tgt_source = tgt_cols['tgt_source'] -%} - -{%- set is_union = dbtvault.is_union(source) -%} --- Generated by dbtvault. -SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_nk, tgt_ldts, tgt_source], 'stg') }} -FROM ( - {{ dbtvault.create_source(src_pk, src_nk, src_ldts, src_source, - tgt_pk, tgt_nk, tgt_ldts, tgt_source, - source, is_union) }} -) AS stg -{# If incremental union or single #} -{%- if is_incremental() -%} -LEFT JOIN {{ this }} AS tgt -ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} -WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL -{# If an incremental and union load -#} -{% if is_union -%} -AND stg.FIRST_SOURCE IS NULL -{%- endif -%} -{%- endif -%} -{# If a union base-load #} -{%- if is_union and not is_incremental() -%} -WHERE stg.FIRST_SOURCE IS NULL -{%- endif -%} -{%- endmacro -%} \ No newline at end of file diff --git a/macros/tables_deprecated/link_template.sql b/macros/tables_deprecated/link_template.sql deleted file mode 100644 index 6813d08f4..000000000 --- a/macros/tables_deprecated/link_template.sql +++ /dev/null @@ -1,49 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro link_template(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source) -%} - -{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_fk=src_fk, src_ldts=src_ldts, src_source=src_source, - tgt_pk=tgt_pk, tgt_fk=tgt_fk, tgt_ldts=tgt_ldts, tgt_source=tgt_source, - source=source) -%} - -{%- set tgt_pk = tgt_cols['tgt_pk'] -%} -{%- set tgt_fk = tgt_cols['tgt_fk'] -%} -{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} -{%- set tgt_source = tgt_cols['tgt_source'] -%} - -{%- set is_union = dbtvault.is_union(source) -%} --- Generated by dbtvault. -SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_ldts, tgt_source], 'stg') }} -FROM ( - {{ dbtvault.create_source(src_pk, src_fk, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_ldts, tgt_source, - source, is_union) }} -) AS stg -{# If incremental union or single #} -{%- if is_incremental() -%} -LEFT JOIN {{ this }} AS tgt -ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} -WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL -{# If an incremental and union load -#} -{% if is_union -%} -AND stg.FIRST_SOURCE IS NULL -{%- endif -%} -{%- endif -%} -{# If a union base-load #} -{%- if is_union and not is_incremental() -%} -WHERE stg.FIRST_SOURCE IS NULL -{%- endif -%} -{%- endmacro -%} \ No newline at end of file diff --git a/macros/tables_deprecated/sat_template.sql b/macros/tables_deprecated/sat_template.sql deleted file mode 100644 index a632e93bf..000000000 --- a/macros/tables_deprecated/sat_template.sql +++ /dev/null @@ -1,61 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro sat_template(src_pk, src_hashdiff, src_payload, - src_eff, src_ldts, src_source, - tgt_pk, tgt_hashdiff, tgt_payload, - tgt_eff, tgt_ldts, tgt_source, - source) -%} - -{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, - src_hashdiff=src_hashdiff, src_payload=src_payload, src_eff=src_eff, - src_ldts=src_ldts, src_source=src_source, - tgt_pk=tgt_pk, - tgt_hashdiff=tgt_hashdiff, tgt_payload=tgt_payload, tgt_eff=tgt_eff, - tgt_ldts=tgt_ldts, tgt_source=tgt_source, - source=source) -%} - -{%- set tgt_pk = tgt_cols['tgt_pk'] -%} -{%- set tgt_hashdiff = tgt_cols['tgt_hashdiff'] -%} -{%- set tgt_payload = tgt_cols['tgt_payload'] -%} -{%- set tgt_eff = tgt_cols['tgt_eff'] -%} -{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} -{%- set tgt_source = tgt_cols['tgt_source'] -%} - -{%- set tgt_cols_list = dbtvault.get_col_list([tgt_pk, tgt_hashdiff, tgt_payload, tgt_eff, tgt_ldts, tgt_source]) -%} - --- Generated by dbtvault. -SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_hashdiff, tgt_payload, tgt_ldts, tgt_eff, tgt_source], 'e') }} -FROM {{ source[0] }} AS e -{% if is_incremental() -%} -LEFT JOIN ( - SELECT {{ dbtvault.prefix(tgt_cols_list, 'd') }} - FROM ( - SELECT {{ dbtvault.prefix(tgt_cols_list, 'c') }}, - CASE WHEN RANK() - OVER (PARTITION BY {{ dbtvault.prefix([tgt_pk|last], 'c') }} - ORDER BY {{ dbtvault.prefix([tgt_ldts|last], 'c') }} DESC) = 1 - THEN 'Y' ELSE 'N' END CURR_FLG - FROM ( - SELECT {{ dbtvault.prefix(tgt_cols_list, 'a') }} - FROM {{ this }} as a - JOIN {{ source[0] }} as b - ON {{ dbtvault.prefix([tgt_pk|last], 'a') }} = {{ dbtvault.prefix([src_pk], 'b') }} - ) as c - ) AS d -WHERE d.CURR_FLG = 'Y') AS src -ON {{ dbtvault.prefix([tgt_hashdiff|last], 'src') }} = {{ dbtvault.prefix([src_hashdiff], 'e') }} -WHERE {{ dbtvault.prefix([tgt_hashdiff|last], 'src') }} IS NULL -{%- endif -%} - -{% endmacro %} diff --git a/macros/tables_deprecated/t_link_template.sql b/macros/tables_deprecated/t_link_template.sql deleted file mode 100644 index e05b4cd2e..000000000 --- a/macros/tables_deprecated/t_link_template.sql +++ /dev/null @@ -1,44 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro t_link_template(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, - tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source, - source) -%} - -{%- set tgt_cols = dbtvault.create_tgt_cols(src_pk=src_pk, src_fk=src_fk, src_payload=src_payload, - src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, - tgt_pk=tgt_pk, tgt_fk=tgt_fk, tgt_payload=tgt_payload, - tgt_eff=tgt_eff, tgt_ldts=tgt_ldts, tgt_source=tgt_source, - source=source) -%} - -{%- set tgt_pk = tgt_cols['tgt_pk'] -%} -{%- set tgt_fk = tgt_cols['tgt_fk'] -%} -{%- set tgt_payload = tgt_cols['tgt_payload'] -%} -{%- set tgt_eff = tgt_cols['tgt_eff'] -%} -{%- set tgt_ldts = tgt_cols['tgt_ldts'] -%} -{%- set tgt_source = tgt_cols['tgt_source'] -%} - -{%- set is_union = dbtvault.is_union(source) -%} --- Generated by dbtvault. -SELECT DISTINCT {{ dbtvault.cast([tgt_pk, tgt_fk, tgt_payload, tgt_eff, tgt_ldts, tgt_source], 'stg') }} -FROM ( - SELECT {{ dbtvault.prefix([src_pk, src_fk, src_payload, src_eff, - src_ldts, src_source], 'stg') }} - FROM {{ source[0] }} AS stg -) AS stg -{% if is_incremental() -%} -LEFT JOIN {{ this }} AS tgt -ON {{ dbtvault.prefix([tgt_pk|first], 'stg') }} = {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} -WHERE {{ dbtvault.prefix([tgt_pk|last], 'tgt') }} IS NULL -{%- endif -%} -{%- endmacro -%} \ No newline at end of file From 89345b33702ed9381368e8163cf27c1207a95447 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 01:16:23 +0100 Subject: [PATCH 132/157] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d1921566e..8f77c33d5 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,10 @@ Get notified of new features and new releases before anyone else! ## Starting a Data Vault project +Looking to use dbtvault or Data Vault in your project? We've written a document to give you a head start. + +Download for FREE now! + ## Contributing [View our contribution guidelines](CONTRIBUTING.md) From fc678ad71d28f760ccb4a5272dffd6287c371c2b Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 01:17:06 +0100 Subject: [PATCH 133/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8f77c33d5..bbec65d9c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) -[past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/) +[past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/stable) # dbtvault by [Datavault](https://www.data-vault.co.uk) From 5bcbd5c8d8cf6a30127626da3ccf733d7bd2a556 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 01:18:24 +0100 Subject: [PATCH 134/157] Removed models_test --- dbt_project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbt_project.yml b/dbt_project.yml index a01723977..03ad7784e 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -4,7 +4,7 @@ require-dbt-version: [">=0.14.0", "<0.17.0"] profile: 'dbtvault' -source-paths: ["models", "models_test"] +source-paths: ["models"] analysis-paths: ["analysis"] test-paths: ["tests"] data-paths: ["data"] From d3b5ce2b1a3731486a31d37c7eb43d4e55e9b589 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 01:19:51 +0100 Subject: [PATCH 135/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bbec65d9c..8e4ae415f 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.5 # Latest stable version + revision: v0.6 # Latest stable version ``` And run From 3d1fb86282f8252bf9b0f937f2ba696e2b74fb74 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 01:20:13 +0100 Subject: [PATCH 136/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e4ae415f..b92ea2c10 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ And run {{- config(...) -}} {{ dbtvault.hub(var('src_pk'), var('src_nk'), var('src_ldts'), - var('src_source'), var('source')) }} + var('src_source'), var('source_model')) }} ``` ## Join our Slack Channel From 37c47b3023915cee0f7d6bb6bee524f266d2c57d Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 01:22:10 +0100 Subject: [PATCH 137/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b92ea2c10..bbeaf0f8d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ powered by [dbt](https://www.getdbt.com/), a registered trademark of [Fishtown A Learn quickly with our worked example: -- [Read the docs](https://dbtvault.readthedocs.io/en/latest/workedexample/) +- [Read the docs](https://dbtvault.readthedocs.io/en/latest/worked_example/we_worked_example/) - [Project Repository](https://github.com/Datavault-UK/snowflakeDemo) From 5134e8deb5bb76f0b53b20ea8080ae470b07c5ca Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 01:28:49 +0100 Subject: [PATCH 138/157] Deleted docs placeholder Coming soon :) --- macros/internal/docs/internal_macros.md | 5 ----- macros/internal/docs/internal_macros_schema.yml | 10 ---------- 2 files changed, 15 deletions(-) delete mode 100644 macros/internal/docs/internal_macros.md delete mode 100644 macros/internal/docs/internal_macros_schema.yml diff --git a/macros/internal/docs/internal_macros.md b/macros/internal/docs/internal_macros.md deleted file mode 100644 index 6b8bbc6f3..000000000 --- a/macros/internal/docs/internal_macros.md +++ /dev/null @@ -1,5 +0,0 @@ -{%- docs macro_alias -%} - -. - -{%- enddocs %} \ No newline at end of file diff --git a/macros/internal/docs/internal_macros_schema.yml b/macros/internal/docs/internal_macros_schema.yml deleted file mode 100644 index fa75207f8..000000000 --- a/macros/internal/docs/internal_macros_schema.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 - -macros: - - name: alias - description: "{{ doc('macro_alias') }}" - - arguments: - - name: src_pk - type: string - description: "" \ No newline at end of file From 2d32bb748efe6e2dcdbd9b164a7846c5d8f48b88 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 10:06:35 +0100 Subject: [PATCH 139/157] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bbeaf0f8d..b465f945c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ [![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) -[past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/stable) +[Past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/stable) +[Changelog](https://dbtvault.readthedocs.io/en/latest/changelog/stable/) # dbtvault by [Datavault](https://www.data-vault.co.uk) From 08b30dbf2c566af820578501911fe37ef5ecf326 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 10:06:49 +0100 Subject: [PATCH 140/157] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b465f945c..684dd0281 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [Past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/stable) + [Changelog](https://dbtvault.readthedocs.io/en/latest/changelog/stable/) # dbtvault by [Datavault](https://www.data-vault.co.uk) From 38571ab9948d555e7964605efaf98aa05b41b9fd Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 May 2020 10:14:44 +0100 Subject: [PATCH 141/157] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 684dd0281..d5585d45c 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,7 @@ [![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) -[Past docs versions](https://dbtvault.readthedocs.io/en/latest/changelog/stable) - -[Changelog](https://dbtvault.readthedocs.io/en/latest/changelog/stable/) +[Changelog and past doc versions](https://dbtvault.readthedocs.io/en/latest/changelog/stable) # dbtvault by [Datavault](https://www.data-vault.co.uk) From bd7d8ee1f60b52917baab0db112bbc97b39a7c17 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Sat, 30 May 2020 11:57:02 +0100 Subject: [PATCH 142/157] Update README.md --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5585d45c..d32bd1743 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,16 @@

-[![Documentation Status](https://readthedocs.org/projects/dbtvault/badge/?version=stable)](https://dbtvault.readthedocs.io/en/latest/?badge=stable) -[![Join our Slack](https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack)](https://join.slack.com/t/dbtvault/shared_invite/enQtODY5MTY3OTIyMzg2LWJlZDMyNzM4YzAzYjgzYTY0MTMzNTNjN2EyZDRjOTljYjY0NDYyYzEwMTlhODMzNGY3MmU2ODNhYWUxYmM2NjA) - +

+ Documentation Status + Join our slack +

[Changelog and past doc versions](https://dbtvault.readthedocs.io/en/latest/changelog/stable) From 648b2df8aa5180ca746ebb90016726b185cbb4f8 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 24 Jun 2020 10:56:31 +0100 Subject: [PATCH 143/157] Added multi-dispatch and updated to dbt 0.17.0 --- dbt_project.yml | 8 +++- macros/internal/alias.sql | 24 ++++++----- macros/internal/alias_all.sql | 10 ++++- macros/internal/as_constant.sql | 8 +++- macros/internal/check_relation.sql | 22 ---------- macros/internal/multikey.sql | 32 +++++++-------- macros/staging/derive_columns.sql | 7 +++- macros/staging/hash_columns.sql | 24 +++++++---- macros/supporting/cast.sql | 64 ------------------------------ macros/supporting/hash.sql | 21 +++++++--- macros/supporting/prefix.sql | 11 ++++- macros/tables/hub.sql | 10 ++++- macros/tables/link.sql | 24 ++++++----- macros/tables/sat.sql | 10 ++++- macros/tables/t_link.sql | 10 ++++- 15 files changed, 136 insertions(+), 149 deletions(-) delete mode 100644 macros/internal/check_relation.sql delete mode 100644 macros/supporting/cast.sql diff --git a/dbt_project.yml b/dbt_project.yml index 03ad7784e..a71afdd03 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,14 +1,18 @@ name: 'dbtvault' version: '0.6' -require-dbt-version: [">=0.14.0", "<0.17.0"] +require-dbt-version: [">=0.14.0", "<0.18.0"] + +# WARNING: THIS MUST REMAIN VERSION 1 UNTIL FURTHER NOTICE +config-version: 1 profile: 'dbtvault' -source-paths: ["models"] +source-paths: ["models", "models_test"] analysis-paths: ["analysis"] test-paths: ["tests"] data-paths: ["data"] macro-paths: ["macros"] +docs-paths: ["docs"] target-path: "target" clean-targets: diff --git a/macros/internal/alias.sql b/macros/internal/alias.sql index 0e43c73e2..85fa4e84b 100644 --- a/macros/internal/alias.sql +++ b/macros/internal/alias.sql @@ -10,18 +10,24 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro alias(source_column=none, prefix=none) -%} +{%- macro alias(alias_config=none, prefix=none) -%} -{%- if source_column -%} + {{- adapter_macro('dbtvault.alias', alias_config=alias_config, prefix=prefix) -}} - {%- if source_column is iterable and source_column is not string -%} +{%- endmacro %} - {%- if source_column['source_column'] and source_column['alias'] -%} +{%- macro default__alias(alias_config=none, prefix=none) -%} + +{%- if alias_config -%} + + {%- if alias_config is iterable and alias_config is not string -%} + + {%- if alias_config['source_column'] and alias_config['alias'] -%} {%- if prefix -%} - {{prefix}}.{{ source_column['source_column'] }} AS {{ source_column['alias'] }} + {{prefix}}.{{ alias_config['source_column'] }} AS {{ alias_config['alias'] }} {%- else -%} - {{ source_column['source_column'] }} AS {{ source_column['alias'] }} + {{ alias_config['source_column'] }} AS {{ alias_config['alias'] }} {%- endif -%} {%- endif -%} @@ -30,11 +36,11 @@ {%- if prefix -%} - {{- dbtvault.prefix([source_column], prefix) -}} + {{- dbtvault.prefix([alias_config], prefix) -}} {%- else -%} - {{ source_column }} + {{ alias_config }} {%- endif -%} @@ -44,7 +50,7 @@ {%- if execute -%} - {{ exceptions.raise_compiler_error("Invalid alias configuration:\nexpected format: {source_column: 'column', alias: 'column_alias'}\ngot: " ~ source_column) }} + {{ exceptions.raise_compiler_error("Invalid alias configuration:\nexpected format: {source_column: 'column', alias: 'column_alias'}\ngot: " ~ alias_config) }} {%- endif -%} diff --git a/macros/internal/alias_all.sql b/macros/internal/alias_all.sql index 6bfd74557..91a60098f 100644 --- a/macros/internal/alias_all.sql +++ b/macros/internal/alias_all.sql @@ -10,12 +10,18 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro alias_all(columns, prefix) -%} +{%- macro alias_all(columns=none, prefix=none) -%} + + {{- adapter_macro('dbtvault.alias_all', columns=columns, prefix=prefix) -}} + +{%- endmacro %} + +{%- macro default__alias_all(columns, prefix) -%} {%- if columns is iterable and columns is not string -%} {%- for column in columns -%} - {{ dbtvault.alias(column, prefix) }} + {{ dbtvault.alias(alias_config=column, prefix=prefix) }} {%- if not loop.last -%} , {% endif -%} {%- endfor -%} diff --git a/macros/internal/as_constant.sql b/macros/internal/as_constant.sql index 7a4304d9f..0db5bde50 100644 --- a/macros/internal/as_constant.sql +++ b/macros/internal/as_constant.sql @@ -10,7 +10,13 @@ See the License for the specific language governing permissions and limitations under the License. -#} -{%- macro as_constant(column_str) -%} +{%- macro as_constant(column_str=none) -%} + + {{- adapter_macro('dbtvault.as_constant', column_str=column_str) -}} + +{%- endmacro %} + +{%- macro default__as_constant(column_str) -%} {% if column_str is not none %} diff --git a/macros/internal/check_relation.sql b/macros/internal/check_relation.sql deleted file mode 100644 index 2f2a8b16f..000000000 --- a/macros/internal/check_relation.sql +++ /dev/null @@ -1,22 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro check_relation(obj) -%} - -{%- if not (obj is mapping and obj.get('metadata', {}).get('type', '').endswith('Relation')) -%} - {{ return(false) }} -{%- else -%} - {{ return(true) }} -{%- endif -%} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/multikey.sql b/macros/internal/multikey.sql index ded071b45..645585692 100644 --- a/macros/internal/multikey.sql +++ b/macros/internal/multikey.sql @@ -10,29 +10,28 @@ See the License for the specific language governing permissions and limitations under the License. -#} +{%- macro multikey(columns=none, aliases=none, type_for=none) -%} -{%- macro multikey(columns, aliases, type_for) -%} + {{- adapter_macro('dbtvault.multikey', columns=columns, aliases=aliases, type_for=type_for) -}} + +{%- endmacro %} + +{%- macro default__multikey(columns, aliases, type_for) -%} {% if type_for == 'join' %} {% for col in columns if not columns is string %} - {% if loop.index == columns|length %} - {{ dbtvault.prefix([col], aliases[0]) }} = {{ dbtvault.prefix([col], aliases[1]) }} - {% else %} - {{ dbtvault.prefix([col], aliases[0]) }} = {{ dbtvault.prefix([col], aliases[1]) }} AND - {% endif %} + {{ dbtvault.prefix([col], aliases[0]) }} = {{ dbtvault.prefix([col], aliases[1]) }} + {% if not loop.last %} AND {% endif %} {% else %} {{ dbtvault.prefix([columns], aliases[0]) }} = {{ dbtvault.prefix([columns], aliases[1]) }} {% endfor %} -{% elif type_for == 'where null'%} +{% elif type_for == 'where null' %} {% for col in columns if not columns is string %} - {% if loop.index == columns|length %} - {{ dbtvault.prefix([col], aliases[0]) }} IS NULL - {% else %} - {{ dbtvault.prefix([col], aliases[0]) }} IS NULL AND - {% endif %} + {{ dbtvault.prefix([col], aliases[0]) }} IS NULL + {% if not loop.last %} AND {% endif %} {% else %} {{ dbtvault.prefix([columns], aliases[0]) }} IS NULL {% endfor %} @@ -40,12 +39,9 @@ {% elif type_for == 'where not null'%} {% for col in columns if not columns is string %} - {% if loop.index == columns|length %} - {{ dbtvault.prefix([col], aliases[0]) }} IS NOT NULL - {% else %} - {{ dbtvault.prefix([col], aliases[0]) }} IS NOT NULL AND - {% endif %} -{% else %} + {{ dbtvault.prefix([col], aliases[0]) }} IS NOT NULL + {% if not loop.last %} AND {% endif %} +{% else %} {{ dbtvault.prefix([columns], aliases[0]) }} IS NOT NULL {% endfor %} {% endif %} diff --git a/macros/staging/derive_columns.sql b/macros/staging/derive_columns.sql index 9edc2f315..914fe7179 100644 --- a/macros/staging/derive_columns.sql +++ b/macros/staging/derive_columns.sql @@ -10,9 +10,14 @@ See the License for the specific language governing permissions and limitations under the License. -#} - {%- macro derive_columns(source_relation=none, columns=none) -%} + {{- adapter_macro('dbtvault.derive_columns', source_relation=source_relation, columns=columns) -}} + +{%- endmacro %} + +{%- macro default__derive_columns(source_relation=none, columns=none) -%} + {%- set exclude_columns = [] -%} {%- set include_columns = [] -%} diff --git a/macros/staging/hash_columns.sql b/macros/staging/hash_columns.sql index 46fd5a45f..fc0692406 100644 --- a/macros/staging/hash_columns.sql +++ b/macros/staging/hash_columns.sql @@ -10,28 +10,38 @@ See the License for the specific language governing permissions and limitations under the License. -#} - {%- macro hash_columns(columns=none) -%} + {{- adapter_macro('dbtvault.hash_columns', columns=columns) -}} + +{%- endmacro %} + +{%- macro default__hash_columns(columns=none) -%} + {%- if columns is mapping -%} {%- for col in columns -%} - {% if columns[col] is mapping and columns[col].hashdiff -%} + {% if columns[col] is mapping and columns[col].is_hashdiff -%} - {{- dbtvault.hash(columns[col]['columns'], col, columns[col]['hashdiff']) -}} + {{- dbtvault.hash(columns=columns[col]['columns'], + alias=col, + is_hashdiff=columns[col]['is_hashdiff']) -}} {%- elif columns[col] is not mapping -%} - {{- dbtvault.hash(columns[col], col, hashdiff=false) -}} + {{- dbtvault.hash(columns=columns[col], + alias=col, + is_hashdiff=false) -}} - {%- elif columns[col] is mapping and not columns[col].hashdiff -%} + {%- elif columns[col] is mapping and not columns[col].is_hashdiff -%} {%- if execute -%} - {%- do exceptions.warn("[" ~ this ~ "] Warning: You provided a list of columns under a 'columns' key, but did not provide the 'hashdiff' flag. Use list syntax for PKs.") -%} + {%- do exceptions.warn("[" ~ this ~ "] Warning: You provided a list of columns under a 'columns' key, but did not provide the 'is_hashdiff' flag. Use list syntax for PKs.") -%} {% endif %} - {{- dbtvault.hash(columns[col]['columns'], col) -}} + {{- dbtvault.hash(columns=columns[col]['columns'], + alias=col) -}} {%- endif -%} diff --git a/macros/supporting/cast.sql b/macros/supporting/cast.sql deleted file mode 100644 index 4fbbba041..000000000 --- a/macros/supporting/cast.sql +++ /dev/null @@ -1,64 +0,0 @@ -{#- Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --#} - -{%- macro cast(columns, prefix=none) -%} - -{#- If a string or list -#} -{%- if columns is iterable -%} - - {#- If only single string provided -#} - {%- if columns is string -%} - - {{- columns -}} - - {%- else -%} - - {%- for column in columns -%} - - {#- Output String if just a string -#} - {%- if column is string -%} - {%- if prefix -%} - {{- dbtvault.prefix([column], prefix) -}} - {%- else -%} - {{- column -}} - {%- endif -%} - - {#- Recurse if a list of lists (i.e. multi-column key) -#} - {%- elif column|first is iterable and column|first is not string -%} - {{- dbtvault.cast(column, prefix) -}} - - {#- Otherwise it is a standard list -#} - {%- else -%} - - {#- Make sure it is a triple -#} - {%- if column|length == 3 -%} - {%- if prefix -%} - CAST({{ dbtvault.prefix([column[0]], prefix) }} AS {{ column[1] }}) AS {{ column[2] }} - {%- else -%} - CAST({{ column[0] }} AS {{ column[1] }}) AS {{ column[2] }} - {%- endif -%} - {%- endif -%} - - {%- endif -%} - - {#- Add trailing comma if not last -#} - {{ ',\n' if not loop.last }} - - {%- endfor -%} - - {%- endif -%} - -{%- endif -%} - -{%- endmacro -%} - diff --git a/macros/supporting/hash.sql b/macros/supporting/hash.sql index 290070046..97851e90f 100644 --- a/macros/supporting/hash.sql +++ b/macros/supporting/hash.sql @@ -10,8 +10,17 @@ See the License for the specific language governing permissions and limitations under the License. #} +{%- macro hash(columns=none, alias=none, is_hashdiff=false) -%} -{%- macro hash(columns=none, alias=none, hashdiff=false) -%} + {% if is_hashdiff is none %} + {%- set is_hashdiff = false -%} + {% endif %} + + {{- adapter_macro('dbtvault.hash', columns=columns, alias=alias, is_hashdiff=is_hashdiff) -}} + +{%- endmacro %} + +{%- macro default__hash(columns, alias, is_hashdiff) -%} {%- set hash = var('hash', 'MD5') -%} @@ -27,15 +36,17 @@ {%- set hash_size = 16 -%} {%- endif -%} +{%- set standardise = "NULLIF(UPPER(TRIM(CAST([EXPRESSION] AS VARCHAR))), '')" %} + {#- Alpha sort columns before hashing if a hashdiff -#} -{%- if hashdiff and columns is iterable and columns is not string -%} +{%- if is_hashdiff and columns is iterable and columns is not string -%} {%- set columns = columns|sort -%} {%- endif -%} {#- If single column to hash -#} {%- if columns is string -%} {%- set column_str = dbtvault.as_constant(columns) -%} - CAST(({{ hash_alg }}(NULLIF(UPPER(TRIM(CAST({{ column_str }} AS VARCHAR))), ''))) AS BINARY({{ hash_size }})) AS {{ alias }} + CAST(({{ hash_alg }}({{ standardise | replace('[EXPRESSION]', column_str) }})) AS BINARY({{ hash_size }})) AS {{ alias }} {#- Else a list of columns to hash -#} {%- else -%} @@ -47,9 +58,9 @@ CAST({{ hash_alg }}(CONCAT( {%- set column_str = dbtvault.as_constant(column) -%} {%- if not loop.last %} - IFNULL(NULLIF(UPPER(TRIM(CAST({{ column_str }} AS VARCHAR))), ''), '^^'), '||', + IFNULL({{ standardise | replace('[EXPRESSION]', column_str) }}, '^^'), '||', {%- else %} - IFNULL(NULLIF(UPPER(TRIM(CAST({{ column_str }} AS VARCHAR))), ''), '^^') )) + IFNULL({{ standardise | replace('[EXPRESSION]', column_str) }}, '^^') )) AS BINARY({{ hash_size }})) AS {{ alias }} {%- endif -%} diff --git a/macros/supporting/prefix.sql b/macros/supporting/prefix.sql index fe081258e..5b024950b 100644 --- a/macros/supporting/prefix.sql +++ b/macros/supporting/prefix.sql @@ -11,7 +11,13 @@ limitations under the License. -#} -{%- macro prefix(columns=none, prefix_str=none, alias_target='source') -%} +{%- macro prefix(columns, prefix_str, alias_target) -%} + + {{- adapter_macro('dbtvault.prefix', columns=columns, prefix_str=prefix_str, alias_target=alias_target) -}} + +{%- endmacro -%} + +{%- macro default__prefix(columns=none, prefix_str=none, alias_target='source') -%} {%- if columns and prefix_str -%} @@ -42,8 +48,9 @@ {{- dbtvault.prefix(col, prefix_str) -}} {%- elif col is not none -%} + {{- prefix_str}}.{{col.strip() -}} - {% else %} + {% else %} {%- if execute -%} {{- exceptions.raise_compiler_error("Unexpected or missing configuration for '" ~ this ~ "' Unable to prefix columns.") -}} diff --git a/macros/tables/hub.sql b/macros/tables/hub.sql index 33b78ded4..2ba855795 100644 --- a/macros/tables/hub.sql +++ b/macros/tables/hub.sql @@ -10,9 +10,15 @@ See the License for the specific language governing permissions and limitations under the License. -#} - {%- macro hub(src_pk, src_nk, src_ldts, src_source, source_model) -%} + {{- adapter_macro('dbtvault.hub', src_pk=src_pk, src_nk=src_nk, + src_ldts=src_ldts, src_source=src_source, source_model=source_model) -}} + +{%- endmacro -%} + +{%- macro default__hub(src_pk, src_nk, src_ldts, src_source, source_model) -%} + {%- set source_cols = dbtvault.expand_column_list([src_pk, src_nk, src_ldts, src_source]) -%} -- Generated by dbtvault. @@ -26,7 +32,7 @@ STG_{{ loop.index|string }} AS ( SELECT DISTINCT {{ dbtvault.prefix(source_cols, 'a') }} FROM ( - SELECT {{ src_pk }}, {{ src_nk }}, {{ src_ldts }}, {{ src_source }}, + SELECT {{ source_cols | join(', ') }}, ROW_NUMBER() OVER( PARTITION BY {{ src_pk }} ORDER BY {{ src_ldts }} ASC diff --git a/macros/tables/link.sql b/macros/tables/link.sql index 9270a9999..d5aa0c2f3 100644 --- a/macros/tables/link.sql +++ b/macros/tables/link.sql @@ -10,10 +10,16 @@ See the License for the specific language governing permissions and limitations under the License. -#} - {%- macro link(src_pk, src_fk, src_ldts, src_source, source_model) -%} -{%- set source_cols = dbtvault.expand_column_list([src_pk, src_fk, src_ldts, src_source]) -%} + {{- adapter_macro('dbtvault.link', src_pk=src_pk, src_fk=src_fk, + src_ldts=src_ldts, src_source=src_source, source_model=source_model) -}} + +{%- endmacro %} + +{%- macro default__link(src_pk, src_fk, src_ldts, src_source, source_model) -%} + +{%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_fk, src_ldts, src_source]) -%} {%- set fk_cols = dbtvault.expand_column_list([src_fk]) -%} -- Generated by dbtvault. @@ -59,12 +65,11 @@ STG AS ( {%- endif %} {%- endfor %} ) - {{ 'WHERE' -}} + {{ 'WHERE ' -}} {%- for fk in fk_cols -%} - {%- if not loop.last %} - {{ fk }} IS NOT NULL AND - {%- else %} {{ fk }} IS NOT NULL + {%- if not loop.last %} + {{ 'AND ' -}} {%- endif -%} {%- endfor %} ) AS b @@ -81,12 +86,11 @@ STG AS ( ORDER BY b.{{ src_ldts }}, b.{{ src_source }} ASC ) AS RN FROM {{ ref(source_model) }} AS b - {{ 'WHERE' -}} + {{ 'WHERE ' -}} {%- for fk in fk_cols -%} - {%- if not loop.last %} - b.{{ fk }} IS NOT NULL AND - {%- else %} b.{{ fk }} IS NOT NULL + {%- if not loop.last %} + {{ 'AND ' -}} {%- endif -%} {%- endfor %} ) AS a diff --git a/macros/tables/sat.sql b/macros/tables/sat.sql index 3e97103e8..fd9a92f28 100644 --- a/macros/tables/sat.sql +++ b/macros/tables/sat.sql @@ -10,10 +10,16 @@ See the License for the specific language governing permissions and limitations under the License. -#} - {%- macro sat(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, source_model) -%} -{%- set source_cols = dbtvault.expand_column_list([src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source]) -%} + {{- adapter_macro('dbtvault.sat', src_pk=src_pk, src_hashdiff=src_hashdiff, src_payload=src_payload, + src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, source_model=source_model) -}} + +{%- endmacro %} + +{%- macro default__sat(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, source_model) -%} + +{%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source]) -%} -- Generated by dbtvault. {% if not is_incremental() -%} diff --git a/macros/tables/t_link.sql b/macros/tables/t_link.sql index 5b9fac9b9..7c216a931 100644 --- a/macros/tables/t_link.sql +++ b/macros/tables/t_link.sql @@ -10,10 +10,16 @@ See the License for the specific language governing permissions and limitations under the License. -#} - {%- macro t_link(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, source_model) -%} -{%- set source_cols = dbtvault.expand_column_list([src_pk, src_fk, src_payload, src_eff, src_ldts, src_source])-%} + {{- adapter_macro('dbtvault.t_link', src_pk=src_pk, src_fk=src_fk, src_payload=src_payload, + src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, source_model=source_model) -}} + +{%- endmacro %} + +{%- macro default__t_link(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, source_model) -%} + +{%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_fk, src_payload, src_eff, src_ldts, src_source])-%} -- Generated by dbtvault. SELECT DISTINCT {{ dbtvault.prefix(source_cols, 'stg') }} From 507a24c904850b190217a66631aca1431d5cace5 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Wed, 24 Jun 2020 18:06:27 +0100 Subject: [PATCH 144/157] Updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d32bd1743..40befaece 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Add the following to your ```packages.yml``` packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.6 # Latest stable version + revision: v0.6.1 # Latest stable version ``` And run From bc0228476750893523df6dff840aa033cc5b9ff3 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 6 Aug 2020 16:34:47 +0100 Subject: [PATCH 145/157] dbt_project.yml fix - Super minor fix which removes 0.17.x requirement for dbtvault. If using vars in dbt_project.yml, you still need to specify config-version: 1 in your own project's dbt_project.yml --- README.md | 8 ++++---- dbt_project.yml | 5 +---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 40befaece..9466f59a4 100644 --- a/README.md +++ b/README.md @@ -42,20 +42,20 @@ Learn quickly with our worked example: ## Installation -Add the following to your ```packages.yml``` +Add the following to your `packages.yml` ```yaml packages: - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.6.1 # Latest stable version + revision: v0.6.2 # Latest stable version ``` And run -```dbt deps``` +`dbt deps` -[Read more on package installation](https://docs.getdbt.com/v0.15.0/docs/package-management) +[Read more on package installation](https://docs.getdbt.com/docs/building-a-dbt-project/package-management/#git-packages) ## Usage diff --git a/dbt_project.yml b/dbt_project.yml index a71afdd03..c82564bf0 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,10 +1,7 @@ name: 'dbtvault' -version: '0.6' +version: '0.6.2' require-dbt-version: [">=0.14.0", "<0.18.0"] -# WARNING: THIS MUST REMAIN VERSION 1 UNTIL FURTHER NOTICE -config-version: 1 - profile: 'dbtvault' source-paths: ["models", "models_test"] From e3c3f77eefa9cbf2838b333a8cf03bd36d190067 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 17 Sep 2020 00:16:01 +0100 Subject: [PATCH 146/157] Update README.md Added circleci shield --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 9466f59a4..ff8b7eaaa 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@

+ Test Status Documentation Status +

+ [Changelog and past doc versions](https://dbtvault.readthedocs.io/en/latest/changelog/stable) # dbtvault by [Datavault](https://www.data-vault.co.uk) From 04f66628d739073e2cd5bc1e0efd88f1b116b5dc Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 17 Sep 2020 00:18:13 +0100 Subject: [PATCH 147/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff8b7eaaa..7ad4f5439 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- Test Status From 6bb076828d37cedfef94414258e64a6cf22d6d10 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Thu, 17 Sep 2020 00:19:45 +0100 Subject: [PATCH 148/157] Update README.md Remove CircleCI badge for now --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 7ad4f5439..91aabc8f1 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,6 @@

- Test Status Documentation Status Date: Fri, 25 Sep 2020 22:03:22 +0100 Subject: [PATCH 149/157] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55fd32e89..909e33c6f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- Documentation Status From ab497cde6d63961ecbb87665dd2d0aea19389ef6 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 28 Sep 2020 11:53:57 +0000 Subject: [PATCH 150/157] Update issue templates Added templates back (they got lost somewhere) --- .github/ISSUE_TEMPLATE/bug_report.md | 30 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 19 ++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..f5c243eb7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] " +labels: '' +assignees: DVAlexHiggs + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Log files** +If applicable, provide dbt log files which include the problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..ce8d98934 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE]" +labels: '' +assignees: DVAlexHiggs + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 67132019a36b2457d09c1d3d6a31dcfe95b11c22 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 28 Sep 2020 11:54:57 +0000 Subject: [PATCH 151/157] Add idea files to ignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5ce60b6e7..9ff28d966 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ invoke.yml Pipfile Pipfile.lock -tasks.py \ No newline at end of file +tasks.py +.idea/ From c071c7dd5377a820c033225bb901fee14377033e Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 28 Sep 2020 12:03:25 +0000 Subject: [PATCH 152/157] Update issue templates Minor template additions --- .github/ISSUE_TEMPLATE/bug_report.md | 5 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 1 + 2 files changed, 6 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f5c243eb7..51bf9417b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,6 +10,11 @@ assignees: DVAlexHiggs **Describe the bug** A clear and concise description of what the bug is. +**Versions** + +dbt: +dbtvault: + **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ce8d98934..967816c37 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -9,6 +9,7 @@ assignees: DVAlexHiggs **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + **Describe the solution you'd like** A clear and concise description of what you want to happen. From 3994eae703dd785846042f25583dfe36c06e666e Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 28 Sep 2020 14:19:54 +0000 Subject: [PATCH 153/157] Update installation instructions --- README.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 909e33c6f..e7d32d004 100644 --- a/README.md +++ b/README.md @@ -44,18 +44,8 @@ Learn quickly with our worked example: ## Installation -Add the following to your `packages.yml` - - -```yaml -packages: - - - git: "https://github.com/Datavault-UK/dbtvault" - revision: v0.7.0 # Latest stable version -``` - -And run -`dbt deps` +Check [dbt Hub](https://hub.getdbt.com/datavault-uk/dbtvault/latest/) for the latest installation instructions, +or read the docs below for more information on installing packages. [Read more on package installation](https://docs.getdbt.com/docs/building-a-dbt-project/package-management/#git-packages) From a90f480832b594b256dfd736a6619766eb30198a Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Mon, 28 Sep 2020 15:35:27 +0100 Subject: [PATCH 154/157] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index e7d32d004..04d7e69f8 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,7 @@ Learn quickly with our worked example: ## Installation Check [dbt Hub](https://hub.getdbt.com/datavault-uk/dbtvault/latest/) for the latest installation instructions, -or read the docs below for more information on installing packages. - -[Read more on package installation](https://docs.getdbt.com/docs/building-a-dbt-project/package-management/#git-packages) +or [read the docs](https://docs.getdbt.com/docs/building-a-dbt-project/package-management/) for more information on installing packages. ## Usage From 7ce6cd3012745086a33526227dddc312d000816f Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 18 Dec 2020 13:45:48 +0000 Subject: [PATCH 155/157] Release 0.7.1 --- CONTRIBUTING.md | 16 ++-- macros/internal/alias.sql | 2 +- macros/internal/alias_all.sql | 2 +- macros/internal/as_constant.sql | 2 +- macros/internal/expand_column_list.sql | 6 +- macros/internal/multikey.sql | 2 +- macros/internal/process_macros.sql | 96 +++++++++++++++++++ macros/materialisations/helpers.sql | 26 ++--- .../helpers_snowflake_schema.yml | 8 +- ...vault_insert_by_period_materialization.sql | 10 +- macros/staging/derive_columns.sql | 45 ++++----- macros/staging/hash_columns.sql | 4 +- macros/staging/source_columns.sql | 16 ++++ macros/staging/stage.sql | 88 ++++++++++++----- macros/staging/staging_snowflake_schema.yml | 6 +- macros/supporting/hash.sql | 4 +- macros/supporting/prefix.sql | 4 +- .../supporting_snowflake_schema.yml | 4 +- macros/tables/eff_sat.sql | 10 +- macros/tables/hub.sql | 8 +- macros/tables/link.sql | 8 +- macros/tables/sat.sql | 8 +- macros/tables/t_link.sql | 8 +- macros/tables/tables_snowflake_schema.yml | 10 +- packages.yml | 4 + 25 files changed, 271 insertions(+), 126 deletions(-) create mode 100644 macros/internal/process_macros.sql create mode 100644 macros/staging/source_columns.sql create mode 100644 packages.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b251dae9..7b3e332a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,16 @@ +## Contributing new features + +Please refer to our contribution guidelines over on our [development repository](https://github.com/Datavault-UK/dbtvault-dev/blob/master/CONTRIBUTING.md) + ## We'd love to hear from you dbtvault is very much a work in progress – we’re constantly adding quality of life improvements and will be adding new table types regularly. -We know that it deserves new features, that the code base can be tidied up and the SQL better tuned. - -Rest assured we’re working on it for future releases – [our roadmap contains information on what’s coming](roadmap.md). +Rest assured we’re working on future releases – [our roadmap contains information on what’s coming](https://dbtvault.readthedocs.io/en/latest/roadmap/). If you spot anything you’d like to bring to our attention, have a request for new features, have spotted an improvement we could make, -or want to tell us about a typo or bug, then please don’t hesitate to let us know via [Github](https://github.com/Datavault-UK/dbtvault/issues). +or want to tell us about a typo or bug, then please don’t hesitate to let us know via [github](https://github.com/Datavault-UK/dbtvault/issues). We’d rather know you are making active use of this package than hearing nothing from all of you out there! @@ -30,8 +32,4 @@ We'd love to add new features to make this package even more useful for the comm please feel free to submit ideas and thoughts! ### If it's an idea, feedback or a general inquiry -Create a post with as much detail as possible; We'll be happy to reply and work with you. - -## Pull requests -If you've developed something which we can add via a pull request, we're more than happy to consider it, but we'd -like to discuss the changes first. \ No newline at end of file +Create a post with as much detail as possible; We'll be happy to reply and work with you. \ No newline at end of file diff --git a/macros/internal/alias.sql b/macros/internal/alias.sql index 76f4ff37c..07e57a1c2 100644 --- a/macros/internal/alias.sql +++ b/macros/internal/alias.sql @@ -1,6 +1,6 @@ {%- macro alias(alias_config=none, prefix=none) -%} - {{- adapter.dispatch('alias', packages = ['dbtvault'])(alias_config=alias_config, prefix=prefix) -}} + {{- adapter.dispatch('alias', packages = var('adapter_packages', ['dbtvault']))(alias_config=alias_config, prefix=prefix) -}} {%- endmacro %} diff --git a/macros/internal/alias_all.sql b/macros/internal/alias_all.sql index 15daadacb..14e4b3fa0 100644 --- a/macros/internal/alias_all.sql +++ b/macros/internal/alias_all.sql @@ -1,6 +1,6 @@ {%- macro alias_all(columns=none, prefix=none) -%} - {{- adapter.dispatch('alias_all', packages = ['dbtvault'])(columns=columns, prefix=prefix) -}} + {{- adapter.dispatch('alias_all', packages = var('adapter_packages', ['dbtvault']))(columns=columns, prefix=prefix) -}} {%- endmacro %} diff --git a/macros/internal/as_constant.sql b/macros/internal/as_constant.sql index 12e3e4403..96b8f1612 100644 --- a/macros/internal/as_constant.sql +++ b/macros/internal/as_constant.sql @@ -1,6 +1,6 @@ {%- macro as_constant(column_str=none) -%} - {{- adapter.dispatch('as_constant', packages = ['dbtvault'])(column_str=column_str) -}} + {{- adapter.dispatch('as_constant', packages = var('adapter_packages', ['dbtvault']))(column_str=column_str) -}} {%- endmacro %} diff --git a/macros/internal/expand_column_list.sql b/macros/internal/expand_column_list.sql index bc96e6868..7b1a2d835 100644 --- a/macros/internal/expand_column_list.sql +++ b/macros/internal/expand_column_list.sql @@ -14,20 +14,20 @@ {%- if col is string -%} - {%- set _ = col_list.append(col) -%} + {%- do col_list.append(col) -%} {#- If list of lists -#} {%- elif col is iterable and col is not string -%} {%- if col is mapping -%} - {%- set _ = col_list.append(col) -%} + {%- do col_list.append(col) -%} {%- else -%} {%- for cols in col -%} - {%- set _ = col_list.append(cols) -%} + {%- do col_list.append(cols) -%} {%- endfor -%} diff --git a/macros/internal/multikey.sql b/macros/internal/multikey.sql index 810e04f58..d57d4e187 100644 --- a/macros/internal/multikey.sql +++ b/macros/internal/multikey.sql @@ -1,6 +1,6 @@ {%- macro multikey(columns, prefix=none, condition=none, operator='AND') -%} - {{- adapter.dispatch('multikey', packages = ['dbtvault'])(columns=columns, prefix=prefix, condition=condition, operator=operator) -}} + {{- adapter.dispatch('multikey', packages = var('adapter_packages', ['dbtvault']))(columns=columns, prefix=prefix, condition=condition, operator=operator) -}} {%- endmacro %} diff --git a/macros/internal/process_macros.sql b/macros/internal/process_macros.sql new file mode 100644 index 000000000..f375c80b0 --- /dev/null +++ b/macros/internal/process_macros.sql @@ -0,0 +1,96 @@ +{%- macro process_excludes(source_relation=none, derived_columns=none, columns=none) -%} + +{%- set exclude_columns_list = [] -%} +{%- set include_columns = [] -%} +{%- if exclude_columns is none -%} + {%- set exclude_columns = false -%} +{% endif %} + +{#- getting all the source columns -#} + +{%- set source_columns = dbtvault.source_columns(source_relation=source_relation) -%} + +{%- if columns is mapping -%} + + {%- for col in columns -%} + + {# Checks if the exclude flag is present and then creates a exclude list to pass to NEED BETTER NAME FOR MACRO #} + {%- if columns[col] is mapping and columns[col].exclude_columns -%} + + {%- for flagged_cols in columns[col]['columns'] -%} + + {%- do exclude_columns_list.append(flagged_cols) -%} + + {%- endfor -%} + + {%- set include_columns = dbtvault.process_include_columns(primary_set_list=derived_columns, secondary_set_list=source_columns, exclude_columns_list=exclude_columns_list) -%} + + {#- Updates the the apropriate hashdiff to contain the columns we do want to hash -#} + {%- do columns[col].update({'columns': include_columns}) -%} + {%- do columns[col].pop('exclude_columns') -%} + {%- set include_columns = [] -%} + {%- set exclude_columns = [] -%} + + {%- endif -%} + {%- endfor -%} +{%- endif -%} + +{%- do return(columns) -%} + + +{%- endmacro -%} + + +{%- macro process_include_columns(primary_set_list=none, secondary_set_list=none, exclude_columns_list=none) -%} + +{%- set include_columns = [] -%} + +{%- if exclude_columns is none -%} + {%- set exclude_columns_list = [] -%} +{%- endif -%} + +{# Appending primary list items not in exclude columns #} +{%- if primary_set_list is not none -%} + + {%- for primary_col in primary_set_list -%} + + {%- if primary_col not in exclude_columns_list -%} + + {%- if primary_set_list is mapping -%} + {%- set primary_str = dbtvault.as_constant(primary_col) -%} + {%- do include_columns.append(primary_str) -%} + {%- do exclude_columns_list.append(primary_str) -%} + {%- else -%} + {%- do include_columns.append(primary_col) -%} + {%- do exclude_columns_list.append(primary_col) -%} + {%- endif -%} + + {%- endif -%} + + {%- endfor -%} + +{%- endif -%} + +{# Apending the secondary list items not in the priamry list or the exclude list #} +{%- if secondary_set_list is not none -%} + + {%- for secondary_col in secondary_set_list -%} + + {%- if secondary_col not in exclude_columns_list -%} + + {%- if secondary_set_list is mapping -%} + {%- set secondary_str = dbtvault.as_constant(secondary_col) -%} + {%- do include_columns.append(secondary_str) -%} + {%- else -%} + {%- do include_columns.append(secondary_col) -%} + {%- endif -%} + + {%- endif -%} + + {% endfor -%} + +{%- endif -%} + +{%- do return(include_columns) -%} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/materialisations/helpers.sql b/macros/materialisations/helpers.sql index 4b66c8661..c8be30a78 100644 --- a/macros/materialisations/helpers.sql +++ b/macros/materialisations/helpers.sql @@ -7,16 +7,16 @@ {%- macro replace_placeholder_with_filter(core_sql, timestamp_field, start_timestamp, stop_timestamp, offset, period) -%} {% set macro = adapter.dispatch('replace_placeholder_with_filter', - packages = ['dbtvault'])(core_sql=core_sql, - timestamp_field=timestamp_field, - start_timestamp=start_timestamp, - stop_timestamp=stop_timestamp, - offset=offset, - period=period) %} + packages = var('adapter_packages', ['dbtvault']))(core_sql=core_sql, + timestamp_field=timestamp_field, + start_timestamp=start_timestamp, + stop_timestamp=stop_timestamp, + offset=offset, + period=period) %} {% do return(macro) %} {%- endmacro %} -{% macro snowflake__replace_placeholder_with_filter(core_sql, timestamp_field, start_timestamp, stop_timestamp, offset, period) %} +{% macro default__replace_placeholder_with_filter(core_sql, timestamp_field, start_timestamp, stop_timestamp, offset, period) %} {%- set period_filter -%} (TO_DATE({{ timestamp_field }}) >= DATE_TRUNC('{{ period }}', TO_DATE('{{ start_timestamp }}') + INTERVAL '{{ offset }} {{ period }}') AND @@ -35,7 +35,7 @@ {%- macro get_period_filter_sql(target_cols_csv, base_sql, timestamp_field, period, start_timestamp, stop_timestamp, offset) -%} {% set macro = adapter.dispatch('get_period_filter_sql', - packages = ['dbtvault'])(target_cols_csv=target_cols_csv, + packages = var('adapter_packages', ['dbtvault']))(target_cols_csv=target_cols_csv, base_sql=base_sql, timestamp_field=timestamp_field, period=period, @@ -45,7 +45,7 @@ {% do return(macro) %} {%- endmacro %} -{% macro snowflake__get_period_filter_sql(target_cols_csv, base_sql, timestamp_field, period, start_timestamp, stop_timestamp, offset) -%} +{% macro default__get_period_filter_sql(target_cols_csv, base_sql, timestamp_field, period, start_timestamp, stop_timestamp, offset) -%} {%- set filtered_sql = {'sql': base_sql} -%} @@ -63,7 +63,7 @@ {%- macro get_period_boundaries(target_schema, target_table, timestamp_field, start_date, stop_date, period) -%} {% set macro = adapter.dispatch('get_period_boundaries', - packages = ['dbtvault'])(target_schema=target_schema, + packages = var('adapter_packages', ['dbtvault']))(target_schema=target_schema, target_table=target_table, timestamp_field=timestamp_field, start_date=start_date, @@ -73,7 +73,7 @@ {% do return(macro) %} {%- endmacro %} -{% macro snowflake__get_period_boundaries(target_schema, target_table, timestamp_field, start_date, stop_date, period) -%} +{% macro default__get_period_boundaries(target_schema, target_table, timestamp_field, start_date, stop_date, period) -%} {% set period_boundary_sql -%} with data as ( @@ -107,14 +107,14 @@ {%- macro get_period_of_load(period, offset, start_timestamp) -%} {% set macro = adapter.dispatch('get_period_of_load', - packages = ['dbtvault'])(period=period, + packages = var('adapter_packages', ['dbtvault']))(period=period, offset=offset, start_timestamp=start_timestamp) %} {% do return(macro) %} {%- endmacro %} -{%- macro snowflake__get_period_of_load(period, offset, start_timestamp) -%} +{%- macro default__get_period_of_load(period, offset, start_timestamp) -%} {% set period_of_load_sql -%} SELECT DATE_TRUNC('{{ period }}', DATEADD({{ period }}, {{ offset }}, TO_DATE('{{start_timestamp}}'))) AS period_of_load diff --git a/macros/materialisations/helpers_snowflake_schema.yml b/macros/materialisations/helpers_snowflake_schema.yml index 99d6d41a2..29ea4746f 100644 --- a/macros/materialisations/helpers_snowflake_schema.yml +++ b/macros/materialisations/helpers_snowflake_schema.yml @@ -1,7 +1,7 @@ version: 2 macros: - - name: snowflake__replace_placeholder_with_filter + - name: default__replace_placeholder_with_filter description: | {{ doc("macro__replace_placeholder_with_filter") }} @@ -26,7 +26,7 @@ macros: type: string description: '{{ doc("arg__period_materialisation__period") }}' - - name: snowflake__get_period_filter_sql + - name: default__get_period_filter_sql description: | {{ doc("macro__get_period_filter_sql") }} @@ -54,7 +54,7 @@ macros: type: string description: '{{ doc("arg__period_materialisation__offset") }}' - - name: snowflake__get_period_boundaries + - name: default__get_period_boundaries description: | {{ doc("macro__get_period_boundaries") }} @@ -79,7 +79,7 @@ macros: type: string description: '{{ doc("arg__period_materialisation__period") }}' - - name: snowflake__get_period_of_load + - name: default__get_period_of_load description: | {{ doc("macro__get_period_of_load") }} diff --git a/macros/materialisations/vault_insert_by_period_materialization.sql b/macros/materialisations/vault_insert_by_period_materialization.sql index 66cbf2da8..fa0498885 100644 --- a/macros/materialisations/vault_insert_by_period_materialization.sql +++ b/macros/materialisations/vault_insert_by_period_materialization.sql @@ -29,6 +29,8 @@ 0, period) %} {% set build_sql = create_table_as(False, target_relation, filtered_sql) %} + {% do to_drop.append(tmp_relation) %} + {% elif existing_relation.is_view or full_refresh_mode %} {#-- Make sure the backup doesn't exist so we don't encounter issues with the rename below #} {% set backup_identifier = existing_relation.identifier ~ "__dbt_backup" %} @@ -43,6 +45,7 @@ 0, period) %} {% set build_sql = create_table_as(False, target_relation, filtered_sql) %} + {% do to_drop.append(tmp_relation) %} {% do to_drop.append(backup_relation) %} {% else %} @@ -88,13 +91,14 @@ {%- set rows_inserted = (load_result(insert_query_name)['status'].split(" "))[1] | int -%} {%- set sum_rows_inserted = loop_vars['sum_rows_inserted'] + rows_inserted -%} - {%- set _ = loop_vars.update({'sum_rows_inserted': sum_rows_inserted}) %} + {%- do loop_vars.update({'sum_rows_inserted': sum_rows_inserted}) %} {{ dbt_utils.log_info("Ran for {} {} of {} ({}); {} records inserted [{}]".format(period, iteration_number, period_boundaries.num_periods, period_of_load, rows_inserted, model.unique_id)) }} + {% do to_drop.append(tmp_relation) %} {% do adapter.commit() %} {% endfor %} @@ -123,7 +127,9 @@ {{ run_hooks(post_hooks, inside_transaction=True) }} {% for rel in to_drop %} - {{ drop_relation_if_exists(backup_relation) }} + {% if rel.type is not none %} + {% do adapter.drop_relation(rel) %} + {% endif %} {% endfor %} {{ run_hooks(post_hooks, inside_transaction=False) }} diff --git a/macros/staging/derive_columns.sql b/macros/staging/derive_columns.sql index b5fa9c91d..8facdfb80 100644 --- a/macros/staging/derive_columns.sql +++ b/macros/staging/derive_columns.sql @@ -1,40 +1,43 @@ {%- macro derive_columns(source_relation=none, columns=none) -%} - {{- adapter.dispatch('derive_columns', packages = ['dbtvault'])(source_relation=source_relation, columns=columns) -}} + {{- adapter.dispatch('derive_columns', packages = var('adapter_packages', ['dbtvault']))(source_relation=source_relation, columns=columns) -}} {%- endmacro %} -{%- macro snowflake__derive_columns(source_relation=none, columns=none) -%} +{%- macro default__derive_columns(source_relation=none, columns=none) -%} {%- set exclude_columns = [] -%} {%- set include_columns = [] -%} +{%- set src_columns = [] -%} +{%- set der_columns = [] -%} -{%- if source_relation is defined and source_relation is not none -%} - {%- set source_model_cols = adapter.get_columns_in_relation(source_relation) -%} -{%- endif %} +{%- set source_cols = dbtvault.source_columns(source_relation=source_relation) -%} {%- if columns is mapping and columns is not none -%} - {#- Add aliases of provided columns to excludes and full SQL to includes -#} + {#- Add aliases of derived columns to excludes and full SQL to includes -#} {%- for col in columns -%} {% set column_str = dbtvault.as_constant(columns[col]) %} - {%- set _ = include_columns.append(column_str ~ " AS " ~ col) -%} - {%- set _ = exclude_columns.append(col) -%} + {%- do der_columns.append(column_str ~ " AS " ~ col) -%} + {%- do exclude_columns.append(col) -%} {%- endfor -%} {#- Add all columns from source_model relation -#} {%- if source_relation is defined and source_relation is not none -%} - {%- for source_col in source_model_cols -%} - {%- if source_col.column not in exclude_columns -%} - {%- set _ = include_columns.append(source_col.column) -%} + {%- for col in source_cols -%} + {%- if col not in exclude_columns -%} + {%- do src_columns.append(col) -%} {%- endif -%} {%- endfor -%} - {%- endif %} + {%- endif -%} + + {#- Makes sure the columns are appended in a logical order. Derived columns then source columns -#} + {%- set include_columns = src_columns + der_columns -%} {#- Print out all columns in includes -#} {%- for col in include_columns -%} @@ -43,27 +46,11 @@ {% endif -%} {%- endfor -%} -{%- elif columns is none and source_relation is not none -%} - - {#- Add all columns from source_model relation -#} - {%- for source_col in source_model_cols -%} - {%- if source_col.column not in exclude_columns -%} - {%- set _ = include_columns.append(source_col.column) -%} - {%- endif -%} - {%- endfor -%} - - {#- Print out all columns in includes -#} - {%- for col in include_columns -%} - {{ col }} - {{- ',\n' if not loop.last -}} - - {%- endfor -%} - {%- else -%} {%- if execute -%} {{ exceptions.raise_compiler_error("Invalid column configuration: -expected format: {source_relation: Relation, columns: 'column_mapping'} +expected format: {'source_relation': Relation, 'columns': {column_name: column_value}} got: {'source_relation': " ~ source_relation ~ ", 'columns': " ~ columns ~ "}") }} {%- endif %} diff --git a/macros/staging/hash_columns.sql b/macros/staging/hash_columns.sql index d80dd7db1..40db01ff4 100644 --- a/macros/staging/hash_columns.sql +++ b/macros/staging/hash_columns.sql @@ -1,10 +1,10 @@ {%- macro hash_columns(columns=none) -%} - {{- adapter.dispatch('hash_columns', packages = ['dbtvault'])(columns=columns) -}} + {{- adapter.dispatch('hash_columns', packages = var('adapter_packages', ['dbtvault']))(columns=columns) -}} {%- endmacro %} -{%- macro snowflake__hash_columns(columns=none) -%} +{%- macro default__hash_columns(columns=none) -%} {%- if columns is mapping -%} diff --git a/macros/staging/source_columns.sql b/macros/staging/source_columns.sql new file mode 100644 index 000000000..4385e7cde --- /dev/null +++ b/macros/staging/source_columns.sql @@ -0,0 +1,16 @@ +{%- macro source_columns(source_relation=none) -%} + +{%- set include_columns = [] -%} + +{%- if source_relation is defined and source_relation is not none -%} + {%- set source_model_cols = adapter.get_columns_in_relation(source_relation) -%} +{%- endif %} + +{#- Add all columns from source_model relation -#} +{%- for source_col in source_model_cols -%} + {%- do include_columns.append(source_col.column) -%} +{%- endfor -%} + +{%- do return(include_columns) -%} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/staging/stage.sql b/macros/staging/stage.sql index 8950844e9..d1edd3431 100644 --- a/macros/staging/stage.sql +++ b/macros/staging/stage.sql @@ -4,11 +4,12 @@ {%- set include_source_columns = true -%} {% endif %} - {{- adapter.dispatch('stage', packages = ['dbtvault'])(include_source_columns=include_source_columns, source_model=source_model, hashed_columns=hashed_columns, derived_columns=derived_columns) -}} + {{- adapter.dispatch('stage', packages = var('adapter_packages', ['dbtvault']))(include_source_columns=include_source_columns, source_model=source_model, hashed_columns=hashed_columns, derived_columns=derived_columns) -}} {%- endmacro -%} -{%- macro snowflake__stage(include_source_columns, source_model, hashed_columns, derived_columns) -%} --- Generated by dbtvault. +{%- macro default__stage(include_source_columns, source_model, hashed_columns, derived_columns) -%} + +{{ dbtvault.prepend_generated_by() }} {% if (source_model is none) and execute %} @@ -20,15 +21,13 @@ OR [SOURCES STYLE] source_model: - source_name: source_table_name" + source_name: source_table_name" {%- endset -%} {{- exceptions.raise_compiler_error(error_message) -}} {%- endif -%} -SELECT - -{# Create relation object from provided source_model -#} +{#- Check for source format or ref format and create relation object from source_model -#} {% if source_model is mapping and source_model is not none -%} {%- set source_name = source_model | first -%} @@ -41,28 +40,67 @@ SELECT {%- set source_relation = ref(source_model) -%} {%- endif -%} -{#- Hash columns, if provided -#} -{% if hashed_columns is defined and hashed_columns is not none -%} - - {{ dbtvault.hash_columns(columns=hashed_columns) -}} - {{ "," if derived_columns is defined and source_relation is defined and include_source_columns }} +{#- CTE to add source columns from the source model -#} +WITH stage AS ( + SELECT -{% endif -%} +{% if source_relation is defined -%} + {%- set included_source_columns = dbtvault.source_columns(source_relation=source_relation) -%} -{#- Derive additional columns, if provided -#} -{%- if derived_columns is defined and derived_columns is not none -%} + {%- for col in included_source_columns -%} + {{ ' ' ~ col }} + {{- ',\n' if not loop.last -}} + {%- endfor -%} - {%- if include_source_columns -%} - {{ dbtvault.derive_columns(source_relation=source_relation, columns=derived_columns) }} - {%- else -%} - {{ dbtvault.derive_columns(columns=derived_columns) }} - {%- endif -%} -{#- If source relation is defined but derived_columns is not, add columns from source model. -#} -{%- elif source_relation is defined and include_source_columns is true -%} - - {{ dbtvault.derive_columns(source_relation=source_relation) }} {%- endif %} -FROM {{ source_relation }} + FROM {{ source_relation }} +), + +{# Derive additional columns, if provided, and carry over source columns from previous CTE for use in the hash stage -#} +derived_columns AS ( + SElECT + + {%- if derived_columns is defined and derived_columns is not none -%} + {%- if include_source_columns or hashed_columns is defined and hashed_columns is not none %} + + {{ dbtvault.derive_columns(source_relation=source_relation, columns=derived_columns) | indent(width=4, first=false) }} + {%- else %} + + {{ dbtvault.derive_columns(columns=derived_columns) | indent(4) }} + + {%- endif -%} + + {#- If source relation is defined but derived_columns is not -#} + {%- else -%} + {{ " *" }} + {%- endif %} + + FROM stage +), + +{# Hash columns, if provided, and process exclusion flags if provided -#} +hashed_columns AS ( + SELECT + + {%- if hashed_columns is defined and hashed_columns is not none %} + {{- " *," if include_source_columns -}} + + {%- if derived_columns is defined and derived_columns is not none and include_source_columns is false %} + + {{ dbtvault.derive_columns(columns=derived_columns) | indent(4) }}, + {%- endif %} + + {%- set hashed_columns = dbtvault.process_excludes(source_relation=source_relation, derived_columns=derived_columns, columns=hashed_columns) %} + + {{ dbtvault.hash_columns(columns=hashed_columns) | indent(4) }} + + {%- else -%} + {{ " *" }} + {%- endif %} + + FROM derived_columns +) +SELECT * FROM hashed_columns {%- endmacro -%} \ No newline at end of file diff --git a/macros/staging/staging_snowflake_schema.yml b/macros/staging/staging_snowflake_schema.yml index bab56263a..7cac4792a 100644 --- a/macros/staging/staging_snowflake_schema.yml +++ b/macros/staging/staging_snowflake_schema.yml @@ -1,7 +1,7 @@ version: 2 macros: - - name: snowflake__stage + - name: default__stage description: | {{ doc("macro__stage") }} @@ -20,7 +20,7 @@ macros: type: Mapping description: '{{ doc("arg__stage__derived_columns") }}' - - name: snowflake__hash_columns + - name: default__hash_columns description: | {{ doc("macro__hash_columns") }} @@ -30,7 +30,7 @@ macros: type: list description: '{{ doc("arg__stage__hashed_columns") }}' - - name: snowflake__derive_columns + - name: default__derive_columns description: | {{ doc("macro__derive_columns") }} diff --git a/macros/supporting/hash.sql b/macros/supporting/hash.sql index 9c02af568..fe9d8f8fd 100644 --- a/macros/supporting/hash.sql +++ b/macros/supporting/hash.sql @@ -4,11 +4,11 @@ {%- set is_hashdiff = false -%} {% endif %} - {{- adapter.dispatch('hash', packages = ['dbtvault'])(columns=columns, alias=alias, is_hashdiff=is_hashdiff) -}} + {{- adapter.dispatch('hash', packages = var('adapter_packages', ['dbtvault']))(columns=columns, alias=alias, is_hashdiff=is_hashdiff) -}} {%- endmacro %} -{%- macro snowflake__hash(columns, alias, is_hashdiff) -%} +{%- macro default__hash(columns, alias, is_hashdiff) -%} {%- set hash = var('hash', 'MD5') -%} diff --git a/macros/supporting/prefix.sql b/macros/supporting/prefix.sql index 37b2ce03b..51f0c9c8e 100644 --- a/macros/supporting/prefix.sql +++ b/macros/supporting/prefix.sql @@ -1,10 +1,10 @@ {%- macro prefix(columns, prefix_str, alias_target) -%} - {{- adapter.dispatch('prefix', packages = ['dbtvault'])(columns=columns, prefix_str=prefix_str, alias_target=alias_target) -}} + {{- adapter.dispatch('prefix', packages = var('adapter_packages', ['dbtvault']))(columns=columns, prefix_str=prefix_str, alias_target=alias_target) -}} {%- endmacro -%} -{%- macro snowflake__prefix(columns=none, prefix_str=none, alias_target='source') -%} +{%- macro default__prefix(columns=none, prefix_str=none, alias_target='source') -%} {%- if columns and prefix_str -%} diff --git a/macros/supporting/supporting_snowflake_schema.yml b/macros/supporting/supporting_snowflake_schema.yml index 0beb702cb..d39c2b69f 100644 --- a/macros/supporting/supporting_snowflake_schema.yml +++ b/macros/supporting/supporting_snowflake_schema.yml @@ -1,7 +1,7 @@ version: 2 macros: - - name: snowflake__hash + - name: default__hash description: | {{ doc("macro__hash") }} @@ -17,7 +17,7 @@ macros: type: boolean description: '{{ doc("arg__hash__is_hashdiff") }}' - - name: snowflake__prefix + - name: default__prefix description: | {{ doc("macro__prefix") }} diff --git a/macros/tables/eff_sat.sql b/macros/tables/eff_sat.sql index e92a69e90..70495b920 100644 --- a/macros/tables/eff_sat.sql +++ b/macros/tables/eff_sat.sql @@ -1,12 +1,12 @@ {%- macro eff_sat(src_pk, src_dfk, src_sfk, src_start_date, src_end_date, src_eff, src_ldts, src_source, source_model) -%} - {{- adapter.dispatch('eff_sat', packages = ['dbtvault'])(src_pk=src_pk, src_dfk=src_dfk, src_sfk=src_sfk, - src_start_date=src_start_date, src_end_date=src_end_date, - src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, - source_model=source_model) -}} + {{- adapter.dispatch('eff_sat', packages = var('adapter_packages', ['dbtvault']))(src_pk=src_pk, src_dfk=src_dfk, src_sfk=src_sfk, + src_start_date=src_start_date, src_end_date=src_end_date, + src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, + source_model=source_model) -}} {%- endmacro -%} -{%- macro snowflake__eff_sat(src_pk, src_dfk, src_sfk, src_start_date, src_end_date, src_eff, src_ldts, src_source, source_model) -%} +{%- macro default__eff_sat(src_pk, src_dfk, src_sfk, src_start_date, src_end_date, src_eff, src_ldts, src_source, source_model) -%} {%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_dfk, src_sfk, src_start_date, src_end_date, src_eff, src_ldts, src_source]) -%} {%- set fk_cols = dbtvault.expand_column_list(columns=[src_dfk, src_sfk]) -%} diff --git a/macros/tables/hub.sql b/macros/tables/hub.sql index ef8b03641..8305f612b 100644 --- a/macros/tables/hub.sql +++ b/macros/tables/hub.sql @@ -1,12 +1,12 @@ {%- macro hub(src_pk, src_nk, src_ldts, src_source, source_model) -%} - {{- adapter.dispatch('hub', packages = ['dbtvault'])(src_pk=src_pk, src_nk=src_nk, - src_ldts=src_ldts, src_source=src_source, - source_model=source_model) -}} + {{- adapter.dispatch('hub', packages = var('adapter_packages', ['dbtvault']))(src_pk=src_pk, src_nk=src_nk, + src_ldts=src_ldts, src_source=src_source, + source_model=source_model) -}} {%- endmacro -%} -{%- macro snowflake__hub(src_pk, src_nk, src_ldts, src_source, source_model) -%} +{%- macro default__hub(src_pk, src_nk, src_ldts, src_source, source_model) -%} {%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_nk, src_ldts, src_source]) -%} diff --git a/macros/tables/link.sql b/macros/tables/link.sql index aa1329204..3407a4e7c 100644 --- a/macros/tables/link.sql +++ b/macros/tables/link.sql @@ -1,12 +1,12 @@ {%- macro link(src_pk, src_fk, src_ldts, src_source, source_model) -%} - {{- adapter.dispatch('link', packages = ['dbtvault'])(src_pk=src_pk, src_fk=src_fk, - src_ldts=src_ldts, src_source=src_source, - source_model=source_model) -}} + {{- adapter.dispatch('link', packages = var('adapter_packages', ['dbtvault']))(src_pk=src_pk, src_fk=src_fk, + src_ldts=src_ldts, src_source=src_source, + source_model=source_model) -}} {%- endmacro -%} -{%- macro snowflake__link(src_pk, src_fk, src_ldts, src_source, source_model) -%} +{%- macro default__link(src_pk, src_fk, src_ldts, src_source, source_model) -%} {%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_fk, src_ldts, src_source]) -%} {%- set fk_cols = dbtvault.expand_column_list([src_fk]) -%} diff --git a/macros/tables/sat.sql b/macros/tables/sat.sql index 702f1d50c..d26b4e922 100644 --- a/macros/tables/sat.sql +++ b/macros/tables/sat.sql @@ -1,12 +1,12 @@ {%- macro sat(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, source_model) -%} - {{- adapter.dispatch('sat', packages = ['dbtvault'])(src_pk=src_pk, src_hashdiff=src_hashdiff, - src_payload=src_payload, src_eff=src_eff, src_ldts=src_ldts, - src_source=src_source, source_model=source_model) -}} + {{- adapter.dispatch('sat', packages = var('adapter_packages', ['dbtvault']))(src_pk=src_pk, src_hashdiff=src_hashdiff, + src_payload=src_payload, src_eff=src_eff, src_ldts=src_ldts, + src_source=src_source, source_model=source_model) -}} {%- endmacro %} -{%- macro snowflake__sat(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, source_model) -%} +{%- macro default__sat(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, source_model) -%} {%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source]) -%} diff --git a/macros/tables/t_link.sql b/macros/tables/t_link.sql index e17ca58d7..a00c5406e 100644 --- a/macros/tables/t_link.sql +++ b/macros/tables/t_link.sql @@ -1,12 +1,12 @@ {%- macro t_link(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, source_model) -%} - {{- adapter.dispatch('t_link', packages = ['dbtvault'])(src_pk=src_pk, src_fk=src_fk, src_payload=src_payload, - src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, - source_model=source_model) -}} + {{- adapter.dispatch('t_link', packages = var('adapter_packages', ['dbtvault']))(src_pk=src_pk, src_fk=src_fk, src_payload=src_payload, + src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, + source_model=source_model) -}} {%- endmacro %} -{%- macro snowflake__t_link(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, source_model) -%} +{%- macro default__t_link(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, source_model) -%} {%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_fk, src_payload, src_eff, src_ldts, src_source]) -%} diff --git a/macros/tables/tables_snowflake_schema.yml b/macros/tables/tables_snowflake_schema.yml index 998714da0..dbc860053 100644 --- a/macros/tables/tables_snowflake_schema.yml +++ b/macros/tables/tables_snowflake_schema.yml @@ -1,7 +1,7 @@ version: 2 macros: - - name: snowflake__hub + - name: default__hub description: | {{ doc("macro__hub") }} @@ -23,7 +23,7 @@ macros: type: string description: '{{ doc("arg__tables__source_model") }}' - - name: snowflake__link + - name: default__link description: | {{ doc("macro__link") }} @@ -45,7 +45,7 @@ macros: type: string description: '{{ doc("arg__tables__source_model") }}' - - name: snowflake__sat + - name: default__sat description: | {{ doc("macro__sat") }} @@ -73,7 +73,7 @@ macros: type: string description: '{{ doc("arg__tables__source_model") }}' - - name: snowflake__t_link + - name: default__t_link description: | {{ doc("macro__t_link") }} @@ -101,7 +101,7 @@ macros: type: string description: '{{ doc("arg__tables__source_model") }}' - - name: snowflake__eff_sat + - name: default__eff_sat description: | {{ doc("macro__eff_sat") }} diff --git a/packages.yml b/packages.yml new file mode 100644 index 000000000..e531cd821 --- /dev/null +++ b/packages.yml @@ -0,0 +1,4 @@ +packages: + + - package: fishtown-analytics/dbt_utils + version: 0.6.2 \ No newline at end of file From 3a627052a4bb721d36359ce61218ee222506bdd8 Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Fri, 18 Dec 2020 15:33:49 +0000 Subject: [PATCH 156/157] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 04d7e69f8..61233d008 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,10 @@ src="https://img.shields.io/badge/Slack-Join-yellow?style=flat&logo=slack" alt="Join our slack" /> - + CircleCI

From 425bc814f82e8b0407462c1e68c06066871086ae Mon Sep 17 00:00:00 2001 From: Alex Higgs Date: Tue, 26 Jan 2021 11:19:54 +0000 Subject: [PATCH 157/157] Release 0.7.2 --- dbt_project.yml | 16 +-- .../helpers/get_period_filter_sql.md | 2 +- .../replace_placeholder_with_filter.md | 4 +- docs/staging.md | 2 +- macros/internal/alias.sql | 6 +- macros/internal/alias_all.sql | 12 +- macros/internal/as_constant.sql | 8 +- macros/internal/expand_column_list.sql | 27 ++-- macros/internal/get_package_namespaces.sql | 4 + macros/internal/is_checks.sql | 33 +++++ macros/internal/multikey.sql | 4 +- macros/internal/process_macros.sql | 96 ------------- macros/internal/stage_processing_macros.sql | 89 ++++++++++++ macros/materialisations/helpers_schema.yml | 6 +- .../helpers_snowflake_schema.yml | 6 +- .../{helpers.sql => period_mat_helpers.sql} | 80 +++++------ macros/materialisations/rank_mat_helpers.sql | 78 +++++++++++ macros/materialisations/shared_helpers.sql | 10 ++ ...vault_insert_by_period_materialization.sql | 8 +- .../vault_insert_by_rank_materialization.sql | 125 +++++++++++++++++ macros/staging/derive_columns.sql | 26 ++-- macros/staging/hash_columns.sql | 16 +-- macros/staging/rank_columns.sql | 23 ++++ macros/staging/source_columns.sql | 17 ++- macros/staging/stage.sql | 128 +++++++++++------- macros/supporting/hash.sql | 48 +++++-- macros/supporting/prefix.sql | 4 +- macros/tables/eff_sat.sql | 41 ++++-- macros/tables/hub.sql | 72 ++++++---- macros/tables/link.sql | 76 +++++++---- macros/tables/sat.sql | 56 +++++--- macros/tables/t_link.sql | 9 +- 32 files changed, 762 insertions(+), 370 deletions(-) create mode 100644 macros/internal/get_package_namespaces.sql create mode 100644 macros/internal/is_checks.sql delete mode 100644 macros/internal/process_macros.sql create mode 100644 macros/internal/stage_processing_macros.sql rename macros/materialisations/{helpers.sql => period_mat_helpers.sql} (67%) create mode 100644 macros/materialisations/rank_mat_helpers.sql create mode 100644 macros/materialisations/shared_helpers.sql create mode 100644 macros/materialisations/vault_insert_by_rank_materialization.sql create mode 100644 macros/staging/rank_columns.sql diff --git a/dbt_project.yml b/dbt_project.yml index 380f2947f..d4946d831 100644 --- a/dbt_project.yml +++ b/dbt_project.yml @@ -1,10 +1,9 @@ name: 'dbtvault' -version: '0.7.0' +version: '0.7.2' require-dbt-version: [">=0.18.0", "<0.19.0"] +config-version: 2 -profile: 'dbtvault' - -source-paths: ["models", "models_test"] +source-paths: ["models"] analysis-paths: ["analysis"] test-paths: ["tests"] data-paths: ["data"] @@ -13,9 +12,8 @@ docs-paths: ["docs"] target-path: "target" clean-targets: - - "target" - - "dbt_modules" + - "target" + - "dbt_modules" -models: - vars: - hash: MD5 \ No newline at end of file +vars: + hash: MD5 \ No newline at end of file diff --git a/docs/materialisations/helpers/get_period_filter_sql.md b/docs/materialisations/helpers/get_period_filter_sql.md index eee1aa7a8..e6ca30657 100644 --- a/docs/materialisations/helpers/get_period_filter_sql.md +++ b/docs/materialisations/helpers/get_period_filter_sql.md @@ -1,6 +1,6 @@ {% docs macro__get_period_filter_sql %} -A wrapper around the `replace_placeholder_with_filter` macro which creates a query designed to +A wrapper around the `replace_placeholder_with_period_filter` macro which creates a query designed to build a temporary table, to select the necessary records for the given load cycle. {% enddocs %} diff --git a/docs/materialisations/helpers/replace_placeholder_with_filter.md b/docs/materialisations/helpers/replace_placeholder_with_filter.md index 8c17d4945..f2261e8bc 100644 --- a/docs/materialisations/helpers/replace_placeholder_with_filter.md +++ b/docs/materialisations/helpers/replace_placeholder_with_filter.md @@ -1,4 +1,4 @@ -{% docs macro__replace_placeholder_with_filter %} +{% docs macro__replace_placeholder_with_period_filter %} Replace the `__PERIOD_FILTER__` string present in the given SQL, with a `WHERE` clause which filters data by a specific `period` of time, `offset` from the `start_date`. @@ -6,7 +6,7 @@ specific `period` of time, `offset` from the `start_date`. {% enddocs %} -{% docs arg__replace_placeholder_with_filter__core_sql %} +{% docs arg__replace_placeholder_with_period_filter__core_sql %} SQL string containing the `__PERIOD_FILTER__` string. diff --git a/docs/staging.md b/docs/staging.md index f7dd7b0b2..7db16fcf9 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -3,7 +3,7 @@ A macro to aid in generating a staging layer for the raw vault. Allows users to: - Create new columns from already existing columns (Derived columns) -- Create new hashed columns from already existing columns (Hashed columns) +- Create new hashed columns from already existing columns and provided derived columns (Hashed columns) [Read more online](https://dbtvault.readthedocs.io/en/latest/macros/#stage) diff --git a/macros/internal/alias.sql b/macros/internal/alias.sql index 07e57a1c2..9a786cb13 100644 --- a/macros/internal/alias.sql +++ b/macros/internal/alias.sql @@ -1,14 +1,14 @@ {%- macro alias(alias_config=none, prefix=none) -%} - {{- adapter.dispatch('alias', packages = var('adapter_packages', ['dbtvault']))(alias_config=alias_config, prefix=prefix) -}} + {{- adapter.dispatch('alias', packages = dbtvault.get_dbtvault_namespaces())(alias_config=alias_config, prefix=prefix) -}} {%- endmacro %} {%- macro default__alias(alias_config=none, prefix=none) -%} -{%- if alias_config -%} +{%- if alias_config is defined and alias_config is not none and alias_config -%} - {%- if alias_config is iterable and alias_config is not string -%} + {%- if alias_config is mapping -%} {%- if alias_config['source_column'] and alias_config['alias'] -%} diff --git a/macros/internal/alias_all.sql b/macros/internal/alias_all.sql index 14e4b3fa0..987b02ea4 100644 --- a/macros/internal/alias_all.sql +++ b/macros/internal/alias_all.sql @@ -1,12 +1,12 @@ {%- macro alias_all(columns=none, prefix=none) -%} - {{- adapter.dispatch('alias_all', packages = var('adapter_packages', ['dbtvault']))(columns=columns, prefix=prefix) -}} + {{- adapter.dispatch('alias_all', packages = dbtvault.get_dbtvault_namespaces())(columns=columns, prefix=prefix) -}} {%- endmacro %} {%- macro default__alias_all(columns, prefix) -%} -{%- if columns is iterable and columns is not string -%} +{%- if dbtvault.is_list(columns) -%} {%- for column in columns -%} {{ dbtvault.alias(alias_config=column, prefix=prefix) }} @@ -17,6 +17,12 @@ {{ dbtvault.alias(alias_config=columns, prefix=prefix) }} -{%- endif -%} +{%- else -%} + + {%- if execute -%} + {{ exceptions.raise_compiler_error("Invalid columns object provided. Must be a list or a string.") }} + {%- endif %} + +{%- endif %} {%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/as_constant.sql b/macros/internal/as_constant.sql index 96b8f1612..69e0e7d62 100644 --- a/macros/internal/as_constant.sql +++ b/macros/internal/as_constant.sql @@ -1,12 +1,12 @@ {%- macro as_constant(column_str=none) -%} - {{- adapter.dispatch('as_constant', packages = var('adapter_packages', ['dbtvault']))(column_str=column_str) -}} + {{- adapter.dispatch('as_constant', packages = dbtvault.get_dbtvault_namespaces())(column_str=column_str) -}} {%- endmacro %} {%- macro default__as_constant(column_str) -%} - {% if column_str is not none %} + {% if column_str is not none and column_str is string and column_str %} {%- if column_str | first == "!" -%} @@ -17,6 +17,10 @@ {{- return(column_str) -}} {%- endif -%} + {%- else -%} + {%- if execute -%} + {{ exceptions.raise_compiler_error("Invalid columns_str object provided. Must be a string and not null.") }} + {%- endif %} {%- endif -%} {%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/expand_column_list.sql b/macros/internal/expand_column_list.sql index 7b1a2d835..f1afa7478 100644 --- a/macros/internal/expand_column_list.sql +++ b/macros/internal/expand_column_list.sql @@ -8,7 +8,7 @@ {%- set col_list = [] -%} -{%- if columns is iterable -%} +{%- if dbtvault.is_list(columns) -%} {%- for col in columns -%} @@ -17,25 +17,32 @@ {%- do col_list.append(col) -%} {#- If list of lists -#} - {%- elif col is iterable and col is not string -%} + {%- elif dbtvault.is_list(col) -%} - {%- if col is mapping -%} + {%- for cols in col -%} - {%- do col_list.append(col) -%} + {%- do col_list.append(cols) -%} - {%- else -%} + {%- endfor -%} + {%- elif col is mapping -%} - {%- for cols in col -%} - - {%- do col_list.append(cols) -%} + {%- do col_list.append(col) -%} - {%- endfor -%} + {%- else -%} - {%- endif -%} + {%- if execute -%} + {{ exceptions.raise_compiler_error("Invalid columns object provided. Must be a list of lists, dictionaries or strings.") }} + {%- endif %} {%- endif -%} {%- endfor -%} +{%- else -%} + + {%- if execute -%} + {{ exceptions.raise_compiler_error("Invalid columns object provided. Must be a list.") }} + {%- endif %} + {%- endif -%} {% do return(col_list) %} diff --git a/macros/internal/get_package_namespaces.sql b/macros/internal/get_package_namespaces.sql new file mode 100644 index 000000000..cd9cc3dc8 --- /dev/null +++ b/macros/internal/get_package_namespaces.sql @@ -0,0 +1,4 @@ +{%- macro get_dbtvault_namespaces() -%} + {%- set override_namespaces = var('adapter_packages', []) -%} + {%- do return(override_namespaces + ['dbtvault']) -%} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/is_checks.sql b/macros/internal/is_checks.sql new file mode 100644 index 000000000..a3f3c1228 --- /dev/null +++ b/macros/internal/is_checks.sql @@ -0,0 +1,33 @@ +{%- macro is_list(obj, empty_is_false=false) -%} + + {%- if obj is iterable and obj is not string and obj is not mapping -%} + {%- if obj is none and obj is undefined and not obj and empty_is_false -%} + {%- do return(false) -%} + {%- endif -%} + + {%- do return(true) -%} + {%- else -%} + {%- do return(false) -%} + {%- endif -%} + +{%- endmacro -%} + +{%- macro is_nothing(obj) -%} + + {%- if obj is none or obj is undefined or not obj -%} + {%- do return(true) -%} + {%- else -%} + {%- do return(false) -%} + {%- endif -%} + +{%- endmacro -%} + +{%- macro is_something(obj) -%} + + {%- if obj is not none and obj is defined and obj -%} + {%- do return(true) -%} + {%- else -%} + {%- do return(false) -%} + {%- endif -%} + +{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/multikey.sql b/macros/internal/multikey.sql index d57d4e187..f9c671ce2 100644 --- a/macros/internal/multikey.sql +++ b/macros/internal/multikey.sql @@ -1,6 +1,6 @@ {%- macro multikey(columns, prefix=none, condition=none, operator='AND') -%} - {{- adapter.dispatch('multikey', packages = var('adapter_packages', ['dbtvault']))(columns=columns, prefix=prefix, condition=condition, operator=operator) -}} + {{- adapter.dispatch('multikey', packages = dbtvault.get_dbtvault_namespaces())(columns=columns, prefix=prefix, condition=condition, operator=operator) -}} {%- endmacro %} @@ -20,7 +20,7 @@ {%- if not loop.last %} {{ operator }} {% endif %} {% endfor -%} {%- else -%} - {%- if columns is iterable and columns is not string -%} + {%- if dbtvault.is_list(columns) -%} {%- for col in columns -%} {{ prefix[0] ~ '.' if prefix }}{{ col }} {{ condition if condition else '' }} {%- if not loop.last -%} {{ "\n " ~ operator }} {% endif -%} diff --git a/macros/internal/process_macros.sql b/macros/internal/process_macros.sql deleted file mode 100644 index f375c80b0..000000000 --- a/macros/internal/process_macros.sql +++ /dev/null @@ -1,96 +0,0 @@ -{%- macro process_excludes(source_relation=none, derived_columns=none, columns=none) -%} - -{%- set exclude_columns_list = [] -%} -{%- set include_columns = [] -%} -{%- if exclude_columns is none -%} - {%- set exclude_columns = false -%} -{% endif %} - -{#- getting all the source columns -#} - -{%- set source_columns = dbtvault.source_columns(source_relation=source_relation) -%} - -{%- if columns is mapping -%} - - {%- for col in columns -%} - - {# Checks if the exclude flag is present and then creates a exclude list to pass to NEED BETTER NAME FOR MACRO #} - {%- if columns[col] is mapping and columns[col].exclude_columns -%} - - {%- for flagged_cols in columns[col]['columns'] -%} - - {%- do exclude_columns_list.append(flagged_cols) -%} - - {%- endfor -%} - - {%- set include_columns = dbtvault.process_include_columns(primary_set_list=derived_columns, secondary_set_list=source_columns, exclude_columns_list=exclude_columns_list) -%} - - {#- Updates the the apropriate hashdiff to contain the columns we do want to hash -#} - {%- do columns[col].update({'columns': include_columns}) -%} - {%- do columns[col].pop('exclude_columns') -%} - {%- set include_columns = [] -%} - {%- set exclude_columns = [] -%} - - {%- endif -%} - {%- endfor -%} -{%- endif -%} - -{%- do return(columns) -%} - - -{%- endmacro -%} - - -{%- macro process_include_columns(primary_set_list=none, secondary_set_list=none, exclude_columns_list=none) -%} - -{%- set include_columns = [] -%} - -{%- if exclude_columns is none -%} - {%- set exclude_columns_list = [] -%} -{%- endif -%} - -{# Appending primary list items not in exclude columns #} -{%- if primary_set_list is not none -%} - - {%- for primary_col in primary_set_list -%} - - {%- if primary_col not in exclude_columns_list -%} - - {%- if primary_set_list is mapping -%} - {%- set primary_str = dbtvault.as_constant(primary_col) -%} - {%- do include_columns.append(primary_str) -%} - {%- do exclude_columns_list.append(primary_str) -%} - {%- else -%} - {%- do include_columns.append(primary_col) -%} - {%- do exclude_columns_list.append(primary_col) -%} - {%- endif -%} - - {%- endif -%} - - {%- endfor -%} - -{%- endif -%} - -{# Apending the secondary list items not in the priamry list or the exclude list #} -{%- if secondary_set_list is not none -%} - - {%- for secondary_col in secondary_set_list -%} - - {%- if secondary_col not in exclude_columns_list -%} - - {%- if secondary_set_list is mapping -%} - {%- set secondary_str = dbtvault.as_constant(secondary_col) -%} - {%- do include_columns.append(secondary_str) -%} - {%- else -%} - {%- do include_columns.append(secondary_col) -%} - {%- endif -%} - - {%- endif -%} - - {% endfor -%} - -{%- endif -%} - -{%- do return(include_columns) -%} - -{%- endmacro -%} \ No newline at end of file diff --git a/macros/internal/stage_processing_macros.sql b/macros/internal/stage_processing_macros.sql new file mode 100644 index 000000000..15f782a08 --- /dev/null +++ b/macros/internal/stage_processing_macros.sql @@ -0,0 +1,89 @@ +{%- macro process_columns_to_select(columns_list=none, exclude_columns_list=none) -%} + + {% set columns_to_select = [] %} + + {% if not dbtvault.is_list(columns_list) or not dbtvault.is_list(exclude_columns_list) %} + + {{- exceptions.raise_compiler_error("One or both arguments are not of list type.") -}} + + {%- endif -%} + + {%- if dbtvault.is_something(columns_list) and dbtvault.is_something(exclude_columns_list) -%} + + {%- for col in columns_list -%} + + {%- if col not in exclude_columns_list -%} + {%- do columns_to_select.append(col) -%} + {%- endif -%} + + {%- endfor -%} + + {%- endif -%} + + {%- do return(columns_to_select) -%} + +{%- endmacro -%} + + +{%- macro extract_column_names(columns_dict=none) -%} + + {%- set extracted_column_names = [] -%} + + {%- if columns_dict is mapping -%} + {%- for key, value in columns_dict.items() -%} + {%- do extracted_column_names.append(key) -%} + {%- endfor -%} + + {%- do return(extracted_column_names) -%} + {%- else -%} + {%- do return([]) -%} + {%- endif -%} + +{%- endmacro -%} + + +{%- macro process_hash_column_excludes(hash_columns=none, source_columns=none) -%} + + {%- set processed_hash_columns = {} -%} + + {%- for col, col_mapping in hash_columns.items() -%} + + {%- if col_mapping is mapping -%} + {%- if col_mapping.exclude_columns -%} + + {%- if col_mapping.columns -%} + + {%- set columns_to_hash = dbtvault.process_columns_to_select(source_columns, col_mapping.columns) -%} + + {%- do hash_columns[col].pop('exclude_columns') -%} + {%- do hash_columns[col].update({'columns': columns_to_hash}) -%} + + {%- do processed_hash_columns.update({col: hash_columns[col]}) -%} + {%- else -%} + + {%- do hash_columns[col].pop('exclude_columns') -%} + {%- do hash_columns[col].update({'columns': source_columns}) -%} + + {%- do processed_hash_columns.update({col: hash_columns[col]}) -%} + {%- endif -%} + {%- else -%} + {%- do processed_hash_columns.update({col: col_mapping}) -%} + {%- endif -%} + {%- else -%} + {%- do processed_hash_columns.update({col: col_mapping}) -%} + {%- endif -%} + + {%- endfor -%} + + {%- do return(processed_hash_columns) -%} + +{%- endmacro -%} + + +{%- macro print_list(list_to_print=none, indent=4) -%} + + {%- for col_name in list_to_print -%} + {{- col_name | indent(indent) -}}{{ ",\n " if not loop.last }} + {%- endfor -%} + +{%- endmacro -%} diff --git a/macros/materialisations/helpers_schema.yml b/macros/materialisations/helpers_schema.yml index 051681f60..3dee27854 100644 --- a/macros/materialisations/helpers_schema.yml +++ b/macros/materialisations/helpers_schema.yml @@ -1,12 +1,12 @@ version: 2 macros: - - name: replace_placeholder_with_filter - description: '{{ doc("macro__replace_placeholder_with_filter") }}' + - name: replace_placeholder_with_period_filter + description: '{{ doc("macro__replace_placeholder_with_period_filter") }}' arguments: - name: core_sql type: string - description: '{{ doc("arg__replace_placeholder_with_filter__core_sql") }}' + description: '{{ doc("arg__replace_placeholder_with_period_filter__core_sql") }}' - name: timestamp_field type: string description: '{{ doc("arg__period_materialisation__timestamp_field") }}' diff --git a/macros/materialisations/helpers_snowflake_schema.yml b/macros/materialisations/helpers_snowflake_schema.yml index 29ea4746f..099849275 100644 --- a/macros/materialisations/helpers_snowflake_schema.yml +++ b/macros/materialisations/helpers_snowflake_schema.yml @@ -1,15 +1,15 @@ version: 2 macros: - - name: default__replace_placeholder_with_filter + - name: default__replace_placeholder_with_period_filter description: | - {{ doc("macro__replace_placeholder_with_filter") }} + {{ doc("macro__replace_placeholder_with_period_filter") }} {{ doc("platform__snowflake") }} arguments: - name: core_sql type: string - description: '{{ doc("arg__replace_placeholder_with_filter__core_sql") }}' + description: '{{ doc("arg__replace_placeholder_with_period_filter__core_sql") }}' - name: timestamp_field type: string description: '{{ doc("arg__period_materialisation__timestamp_field") }}' diff --git a/macros/materialisations/helpers.sql b/macros/materialisations/period_mat_helpers.sql similarity index 67% rename from macros/materialisations/helpers.sql rename to macros/materialisations/period_mat_helpers.sql index c8be30a78..e3532e7a3 100644 --- a/macros/materialisations/helpers.sql +++ b/macros/materialisations/period_mat_helpers.sql @@ -1,22 +1,22 @@ -{#-- Helper macros for custom materializations #} +{#-- Helper macros for period materializations #} {#-- MULTI-DISPATCH MACROS #} -{#-- REPLACE_PLACEHOLDER_WITH_FILTER #} +{#-- REPLACE_PLACEHOLDER_WITH_PERIOD_FILTER #} -{%- macro replace_placeholder_with_filter(core_sql, timestamp_field, start_timestamp, stop_timestamp, offset, period) -%} +{%- macro replace_placeholder_with_period_filter(core_sql, timestamp_field, start_timestamp, stop_timestamp, offset, period) -%} - {% set macro = adapter.dispatch('replace_placeholder_with_filter', - packages = var('adapter_packages', ['dbtvault']))(core_sql=core_sql, - timestamp_field=timestamp_field, - start_timestamp=start_timestamp, - stop_timestamp=stop_timestamp, - offset=offset, - period=period) %} + {% set macro = adapter.dispatch('replace_placeholder_with_period_filter', + packages = dbtvault.get_dbtvault_namespaces())(core_sql=core_sql, + timestamp_field=timestamp_field, + start_timestamp=start_timestamp, + stop_timestamp=stop_timestamp, + offset=offset, + period=period) %} {% do return(macro) %} {%- endmacro %} -{% macro default__replace_placeholder_with_filter(core_sql, timestamp_field, start_timestamp, stop_timestamp, offset, period) %} +{% macro default__replace_placeholder_with_period_filter(core_sql, timestamp_field, start_timestamp, stop_timestamp, offset, period) %} {%- set period_filter -%} (TO_DATE({{ timestamp_field }}) >= DATE_TRUNC('{{ period }}', TO_DATE('{{ start_timestamp }}') + INTERVAL '{{ offset }} {{ period }}') AND @@ -35,13 +35,13 @@ {%- macro get_period_filter_sql(target_cols_csv, base_sql, timestamp_field, period, start_timestamp, stop_timestamp, offset) -%} {% set macro = adapter.dispatch('get_period_filter_sql', - packages = var('adapter_packages', ['dbtvault']))(target_cols_csv=target_cols_csv, - base_sql=base_sql, - timestamp_field=timestamp_field, - period=period, - start_timestamp=start_timestamp, - stop_timestamp=stop_timestamp, - offset=offset) %} + packages = dbtvault.get_dbtvault_namespaces())(target_cols_csv=target_cols_csv, + base_sql=base_sql, + timestamp_field=timestamp_field, + period=period, + start_timestamp=start_timestamp, + stop_timestamp=stop_timestamp, + offset=offset) %} {% do return(macro) %} {%- endmacro %} @@ -49,11 +49,11 @@ {%- set filtered_sql = {'sql': base_sql} -%} - {%- do filtered_sql.update({'sql': dbtvault.replace_placeholder_with_filter(filtered_sql.sql, - timestamp_field, - start_timestamp, - stop_timestamp, - offset, period)}) -%} + {%- do filtered_sql.update({'sql': dbtvault.replace_placeholder_with_period_filter(filtered_sql.sql, + timestamp_field, + start_timestamp, + stop_timestamp, + offset, period)}) -%} select {{ target_cols_csv }} from ({{ filtered_sql.sql }}) {%- endmacro %} @@ -63,12 +63,12 @@ {%- macro get_period_boundaries(target_schema, target_table, timestamp_field, start_date, stop_date, period) -%} {% set macro = adapter.dispatch('get_period_boundaries', - packages = var('adapter_packages', ['dbtvault']))(target_schema=target_schema, - target_table=target_table, - timestamp_field=timestamp_field, - start_date=start_date, - stop_date=stop_date, - period=period) %} + packages = dbtvault.get_dbtvault_namespaces())(target_schema=target_schema, + target_table=target_table, + timestamp_field=timestamp_field, + start_date=start_date, + stop_date=stop_date, + period=period) %} {% do return(macro) %} {%- endmacro %} @@ -79,7 +79,7 @@ with data as ( select coalesce(max({{ timestamp_field }}), '{{ start_date }}')::timestamp as start_timestamp, - coalesce({{ dbt_utils.dateadd('millisecond', 86399999, "nullif('" ~ stop_date ~ "','')::timestamp") }}, + coalesce({{ dbt_utils.dateadd('millisecond', 86399999, "nullif('" ~ stop_date | lower ~ "','none')::timestamp") }}, {{ dbt_utils.current_timestamp() }} ) as stop_timestamp from {{ target_schema }}.{{ target_table }} ) @@ -107,9 +107,9 @@ {%- macro get_period_of_load(period, offset, start_timestamp) -%} {% set macro = adapter.dispatch('get_period_of_load', - packages = var('adapter_packages', ['dbtvault']))(period=period, - offset=offset, - start_timestamp=start_timestamp) %} + packages = dbtvault.get_dbtvault_namespaces())(period=period, + offset=offset, + start_timestamp=start_timestamp) %} {% do return(macro) %} {%- endmacro %} @@ -145,18 +145,6 @@ {% endmacro %} -{% macro check_placeholder(model_sql, placeholder='__PERIOD_FILTER__') %} - - {%- if model_sql.find(placeholder) == -1 -%} - {%- set error_message -%} - Model '{{ model.unique_id }}' does not include the required string '__PERIOD_FILTER__' in its sql - {%- endset -%} - {{ exceptions.raise_compiler_error(error_message) }} - {%- endif -%} - -{% endmacro %} - - {% macro get_start_stop_dates(timestamp_field, date_source_models) %} {% if config.get('start_date', default=none) is not none %} @@ -192,7 +180,7 @@ {% else %} {%- if execute -%} - {{ exceptions.raise_compiler_error("Invalid 'vault_insert_by_period' configuration. Must provide 'start_date' and 'stop_date' and/or 'date_source_models' options.") }} + {{ exceptions.raise_compiler_error("Invalid 'vault_insert_by_period' configuration. Must provide 'start_date' and 'stop_date', just 'stop_date', and/or 'date_source_models' options.") }} {%- endif -%} {% endif %} diff --git a/macros/materialisations/rank_mat_helpers.sql b/macros/materialisations/rank_mat_helpers.sql new file mode 100644 index 000000000..78fd9b3ab --- /dev/null +++ b/macros/materialisations/rank_mat_helpers.sql @@ -0,0 +1,78 @@ +{#-- Helper macros for rank materializations #} + +{#-- MULTI-DISPATCH MACROS #} + +{#-- REPLACE_PLACEHOLDER_WITH_RANK_FILTER #} + +{%- macro replace_placeholder_with_rank_filter(core_sql, rank_column, rank_iteration) -%} + + {% set macro = adapter.dispatch('replace_placeholder_with_rank_filter', + packages = dbtvault.get_dbtvault_namespaces())(core_sql=core_sql, + rank_column=rank_column, + rank_iteration=rank_iteration) %} + {% do return(macro) %} +{%- endmacro %} + +{% macro default__replace_placeholder_with_rank_filter(core_sql, rank_column, rank_iteration) %} + + {%- set rank_filter -%} + {{ rank_column }}::INTEGER = {{ rank_iteration }}::INTEGER + {%- endset -%} + + {%- set filtered_sql = core_sql | replace("__RANK_FILTER__", rank_filter) -%} + + {% do return(filtered_sql) %} +{% endmacro %} + + +{#-- OTHER MACROS #} + +{% macro get_min_max_ranks(rank_column, rank_source_models) %} + + {% if rank_source_models is not none %} + + {% if rank_source_models is string %} + {% set rank_source_models = [rank_source_models] %} + {% endif %} + + {% set query_sql %} + WITH stage AS ( + {% for source_model in rank_source_models %} + SELECT {{ rank_column }} FROM {{ ref(source_model) }} + {% if not loop.last %} UNION ALL {% endif %} + {% endfor %}) + + SELECT MIN({{ rank_column }}) AS MIN, MAX({{ rank_column }}) AS MAX + FROM stage + {% endset %} + + {% set min_max_dict = dbt_utils.get_query_results_as_dict(query_sql) %} + + {% set min_rank = min_max_dict['MIN'][0] | string %} + {% set max_rank = min_max_dict['MAX'][0] | string %} + {% set min_max_ranks = {"min_rank": min_rank, "max_rank": max_rank} %} + + {% do return(min_max_ranks) %} + + {% else %} + {%- if execute -%} + {{ exceptions.raise_compiler_error("Invalid 'vault_insert_by_rank' configuration. Must provide 'rank_column', and 'rank_source_models' options.") }} + {%- endif -%} + {% endif %} + +{% endmacro %} + + +{% macro is_vault_insert_by_rank() %} + {#-- do not run introspective queries in parsing #} + {% if not execute %} + {{ return(False) }} + {% else %} + {% set relation = adapter.get_relation(this.database, this.schema, this.table) %} + + {{ return(relation is not none + and relation.type == 'table' + and model.config.materialized == 'vault_insert_by_rank' + and not flags.FULL_REFRESH) }} + {% endif %} +{% endmacro %} diff --git a/macros/materialisations/shared_helpers.sql b/macros/materialisations/shared_helpers.sql new file mode 100644 index 000000000..b28b9cdba --- /dev/null +++ b/macros/materialisations/shared_helpers.sql @@ -0,0 +1,10 @@ +{% macro check_placeholder(model_sql, placeholder='__PERIOD_FILTER__') %} + + {%- if model_sql.find(placeholder) == -1 -%} + {%- set error_message -%} + Model '{{ model.unique_id }}' does not include the required string '{{ placeholder }}' in its sql + {%- endset -%} + {{ exceptions.raise_compiler_error(error_message) }} + {%- endif -%} + +{% endmacro %} \ No newline at end of file diff --git a/macros/materialisations/vault_insert_by_period_materialization.sql b/macros/materialisations/vault_insert_by_period_materialization.sql index fa0498885..5ace50eb8 100644 --- a/macros/materialisations/vault_insert_by_period_materialization.sql +++ b/macros/materialisations/vault_insert_by_period_materialization.sql @@ -23,7 +23,7 @@ {% if existing_relation is none %} - {% set filtered_sql = dbtvault.replace_placeholder_with_filter(sql, timestamp_field, + {% set filtered_sql = dbtvault.replace_placeholder_with_period_filter(sql, timestamp_field, start_stop_dates.start_date, start_stop_dates.stop_date, 0, period) %} @@ -39,7 +39,7 @@ {% do adapter.drop_relation(backup_relation) %} {% do adapter.rename_relation(target_relation, backup_relation) %} - {% set filtered_sql = dbtvault.replace_placeholder_with_filter(sql, timestamp_field, + {% set filtered_sql = dbtvault.replace_placeholder_with_period_filter(sql, timestamp_field, start_stop_dates.start_date, start_stop_dates.stop_date, 0, period) %} @@ -104,7 +104,7 @@ {% endfor %} {% call noop_statement(name='main', status="INSERT {}".format(loop_vars['sum_rows_inserted']) ) -%} - -- no-op + {{ tmp_table_sql }} {%- endcall %} {% endif %} @@ -117,7 +117,7 @@ {%- set rows_inserted = (load_result("main")['status'].split(" "))[1] | int -%} {% call noop_statement(name='main', status="BASE LOAD {}".format(rows_inserted)) -%} - -- no-op + {{ build_sql }} {%- endcall %} -- `COMMIT` happens here diff --git a/macros/materialisations/vault_insert_by_rank_materialization.sql b/macros/materialisations/vault_insert_by_rank_materialization.sql new file mode 100644 index 000000000..23da8d8cb --- /dev/null +++ b/macros/materialisations/vault_insert_by_rank_materialization.sql @@ -0,0 +1,125 @@ +{% materialization vault_insert_by_rank, default -%} + + {%- set full_refresh_mode = flags.FULL_REFRESH -%} + + {%- set target_relation = this -%} + {%- set existing_relation = load_relation(this) -%} + {%- set tmp_relation = make_temp_relation(this) -%} + + {%- set rank_column = config.require('rank_column') -%} + {%- set rank_source_models = config.require('rank_source_models') -%} + + {%- set min_max_ranks = dbtvault.get_min_max_ranks(rank_column, rank_source_models) | as_native -%} + + {%- set to_drop = [] -%} + + {%- do dbtvault.check_placeholder(sql, "__RANK_FILTER__") -%} + + {{ run_hooks(pre_hooks, inside_transaction=False) }} + + -- `BEGIN` happens here: + {{ run_hooks(pre_hooks, inside_transaction=True) }} + + {% if existing_relation is none %} + + {% set filtered_sql = dbtvault.replace_placeholder_with_rank_filter(sql, rank_column, 1) %} + + {% set build_sql = create_table_as(False, target_relation, filtered_sql) %} + + {% do to_drop.append(tmp_relation) %} + + {% elif existing_relation.is_view or full_refresh_mode %} + {#-- Make sure the backup doesn't exist so we don't encounter issues with the rename below #} + {% set backup_identifier = existing_relation.identifier ~ "__dbt_backup" %} + {% set backup_relation = existing_relation.incorporate(path={"identifier": backup_identifier}) %} + + {% do adapter.drop_relation(backup_relation) %} + {% do adapter.rename_relation(target_relation, backup_relation) %} + + {% set filtered_sql = dbtvault.replace_placeholder_with_rank_filter(sql, rank_column, 1) %} + {% set build_sql = create_table_as(False, target_relation, filtered_sql) %} + + {% do to_drop.append(tmp_relation) %} + {% do to_drop.append(backup_relation) %} + {% else %} + + {% set target_columns = adapter.get_columns_in_relation(target_relation) %} + {%- set target_cols_csv = target_columns | map(attribute='quoted') | join(', ') -%} + {%- set loop_vars = {'sum_rows_inserted': 0} -%} + + {% for i in range(min_max_ranks.max_rank | int ) -%} + + {%- set iteration_number = i + 1 -%} + + {%- set filtered_sql = dbtvault.replace_placeholder_with_rank_filter(sql, rank_column, iteration_number) -%} + + {{ dbt_utils.log_info("Running for {} {} of {} on column '{}' [{}]".format('rank', iteration_number, min_max_ranks.max_rank, rank_column, model.unique_id)) }} + + {% set tmp_relation = make_temp_relation(this) %} + + {% call statement() -%} + {{ dbt.create_table_as(True, tmp_relation, filtered_sql) }} + {%- endcall %} + + {{ adapter.expand_target_column_types(from_relation=tmp_relation, + to_relation=target_relation) }} + + {%- set insert_query_name = 'main-' ~ i -%} + {% call statement(insert_query_name, fetch_result=True) -%} + insert into {{ target_relation }} ({{ target_cols_csv }}) + ( + select {{ target_cols_csv }} + from {{ tmp_relation.include(schema=True) }} + ); + {%- endcall %} + + {%- set rows_inserted = (load_result(insert_query_name)['status'].split(" "))[1] | int -%} + + {%- set sum_rows_inserted = loop_vars['sum_rows_inserted'] + rows_inserted -%} + {%- do loop_vars.update({'sum_rows_inserted': sum_rows_inserted}) %} + + {{ dbt_utils.log_info("Ran for {} {} of {}; {} records inserted [{}]".format('rank', iteration_number, + min_max_ranks.max_rank, + rows_inserted, + model.unique_id)) }} + + + {% do to_drop.append(tmp_relation) %} + {% do adapter.commit() %} + + {% endfor %} + + {% call noop_statement(name='main', status="INSERT {}".format(loop_vars['sum_rows_inserted']) ) -%} + {{ filtered_sql }} + {%- endcall %} + + {% endif %} + + {% if build_sql is defined %} + {% call statement("main", fetch_result=True) %} + {{ build_sql }} + {% endcall %} + + {%- set rows_inserted = (load_result("main")['status'].split(" "))[1] | int -%} + + {% call noop_statement(name='main', status="BASE LOAD {}".format(rows_inserted)) -%} + {{ build_sql }} + {%- endcall %} + + -- `COMMIT` happens here + {% do adapter.commit() %} + {% endif %} + + {{ run_hooks(post_hooks, inside_transaction=True) }} + + {% for rel in to_drop %} + {% if rel.type is not none %} + {% do adapter.drop_relation(rel) %} + {% endif %} + {% endfor %} + + {{ run_hooks(post_hooks, inside_transaction=False) }} + + {{ return({'relations': [target_relation]}) }} + +{%- endmaterialization %} \ No newline at end of file diff --git a/macros/staging/derive_columns.sql b/macros/staging/derive_columns.sql index 8facdfb80..1a31e971a 100644 --- a/macros/staging/derive_columns.sql +++ b/macros/staging/derive_columns.sql @@ -1,6 +1,6 @@ {%- macro derive_columns(source_relation=none, columns=none) -%} - {{- adapter.dispatch('derive_columns', packages = var('adapter_packages', ['dbtvault']))(source_relation=source_relation, columns=columns) -}} + {{- adapter.dispatch('derive_columns', packages = dbtvault.get_dbtvault_namespaces())(source_relation=source_relation, columns=columns) -}} {%- endmacro %} @@ -17,11 +17,23 @@ {#- Add aliases of derived columns to excludes and full SQL to includes -#} {%- for col in columns -%} + {%- if dbtvault.is_list(columns[col]) -%} + {%- set column_list = [] -%} - {% set column_str = dbtvault.as_constant(columns[col]) %} + {%- for concat_component in columns[col] -%} + {%- set column_str = dbtvault.as_constant(concat_component) -%} + {%- do column_list.append(column_str) -%} + {%- endfor -%} - {%- do der_columns.append(column_str ~ " AS " ~ col) -%} - {%- do exclude_columns.append(col) -%} + {%- set concat_string = "CONCAT_WS(" ~ "'||', " ~ column_list | join(", ") ~ ") AS " ~ col -%} + + {%- do der_columns.append(concat_string) -%} + {%- set exclude_columns = exclude_columns + columns[col] -%} + {% else %} + {%- set column_str = dbtvault.as_constant(columns[col]) -%} + {%- do der_columns.append(column_str ~ " AS " ~ col) -%} + {%- do exclude_columns.append(col) -%} + {% endif %} {%- endfor -%} @@ -36,14 +48,12 @@ {%- endif -%} - {#- Makes sure the columns are appended in a logical order. Derived columns then source columns -#} + {#- Makes sure the columns are appended in a logical order. Derived columns then source columns -#} {%- set include_columns = src_columns + der_columns -%} {#- Print out all columns in includes -#} {%- for col in include_columns -%} - {{ col }} - {%- if not loop.last -%}, -{% endif -%} + {{- col | indent(4) -}}{{ ",\n" if not loop.last }} {%- endfor -%} {%- else -%} diff --git a/macros/staging/hash_columns.sql b/macros/staging/hash_columns.sql index 40db01ff4..bfd4f3104 100644 --- a/macros/staging/hash_columns.sql +++ b/macros/staging/hash_columns.sql @@ -1,12 +1,12 @@ {%- macro hash_columns(columns=none) -%} - {{- adapter.dispatch('hash_columns', packages = var('adapter_packages', ['dbtvault']))(columns=columns) -}} + {{- adapter.dispatch('hash_columns', packages = dbtvault.get_dbtvault_namespaces())(columns=columns) -}} {%- endmacro %} {%- macro default__hash_columns(columns=none) -%} -{%- if columns is mapping -%} +{%- if columns is mapping and columns is not none -%} {%- for col in columns -%} @@ -18,8 +18,8 @@ {%- elif columns[col] is not mapping -%} - {{- dbtvault.hash(columns=columns[col], - alias=col, + {{- dbtvault.hash(columns=columns[col], + alias=col, is_hashdiff=false) -}} {%- elif columns[col] is mapping and not columns[col].is_hashdiff -%} @@ -28,14 +28,12 @@ {%- do exceptions.warn("[" ~ this ~ "] Warning: You provided a list of columns under a 'columns' key, but did not provide the 'is_hashdiff' flag. Use list syntax for PKs.") -%} {% endif %} - {{- dbtvault.hash(columns=columns[col]['columns'], - alias=col) -}} + {{- dbtvault.hash(columns=columns[col]['columns'], alias=col) -}} {%- endif -%} - {%- if not loop.last -%}, -{% endif %} + {{- ",\n" if not loop.last -}} {%- endfor -%} -{%- endif -%} +{%- endif %} {%- endmacro -%} diff --git a/macros/staging/rank_columns.sql b/macros/staging/rank_columns.sql new file mode 100644 index 000000000..4cbbe959b --- /dev/null +++ b/macros/staging/rank_columns.sql @@ -0,0 +1,23 @@ +{%- macro rank_columns(columns=none) -%} + + {{- adapter.dispatch('rank_columns', packages = dbtvault.get_dbtvault_namespaces())(columns=columns) -}} + +{%- endmacro %} + +{%- macro default__rank_columns(columns=none) -%} + +{%- if columns is mapping and columns is not none -%} + + {%- for col in columns -%} + + {%- if columns[col] is mapping and columns[col].partition_by and columns[col].order_by -%} + + {{- "RANK() OVER (PARTITION BY {} ORDER BY {}) AS {}".format(columns[col].partition_by, columns[col].order_by, col) | indent(4) -}} + + {%- endif -%} + + {{- ",\n" if not loop.last -}} + {%- endfor -%} + +{%- endif %} +{%- endmacro -%} diff --git a/macros/staging/source_columns.sql b/macros/staging/source_columns.sql index 4385e7cde..32dc43d46 100644 --- a/macros/staging/source_columns.sql +++ b/macros/staging/source_columns.sql @@ -1,16 +1,15 @@ {%- macro source_columns(source_relation=none) -%} -{%- set include_columns = [] -%} + {%- if source_relation -%} + {%- set source_model_cols = adapter.get_columns_in_relation(source_relation) -%} -{%- if source_relation is defined and source_relation is not none -%} - {%- set source_model_cols = adapter.get_columns_in_relation(source_relation) -%} -{%- endif %} + {%- set column_list = [] -%} -{#- Add all columns from source_model relation -#} -{%- for source_col in source_model_cols -%} - {%- do include_columns.append(source_col.column) -%} -{%- endfor -%} + {%- for source_col in source_model_cols -%} + {%- do column_list.append(source_col.column) -%} + {%- endfor -%} -{%- do return(include_columns) -%} + {%- do return(column_list) -%} + {%- endif %} {%- endmacro -%} \ No newline at end of file diff --git a/macros/staging/stage.sql b/macros/staging/stage.sql index d1edd3431..1e961664d 100644 --- a/macros/staging/stage.sql +++ b/macros/staging/stage.sql @@ -1,13 +1,17 @@ -{%- macro stage(include_source_columns=none, source_model=none, hashed_columns=none, derived_columns=none) -%} +{%- macro stage(include_source_columns=none, source_model=none, hashed_columns=none, derived_columns=none, ranked_columns=none) -%} - {% if include_source_columns is none %} + {%- if include_source_columns is none -%} {%- set include_source_columns = true -%} - {% endif %} + {%- endif -%} - {{- adapter.dispatch('stage', packages = var('adapter_packages', ['dbtvault']))(include_source_columns=include_source_columns, source_model=source_model, hashed_columns=hashed_columns, derived_columns=derived_columns) -}} + {{- adapter.dispatch('stage', packages = dbtvault.get_dbtvault_namespaces())(include_source_columns=include_source_columns, + source_model=source_model, + hashed_columns=hashed_columns, + derived_columns=derived_columns, + ranked_columns=ranked_columns) -}} {%- endmacro -%} -{%- macro default__stage(include_source_columns, source_model, hashed_columns, derived_columns) -%} +{%- macro default__stage(include_source_columns, source_model, hashed_columns, derived_columns, ranked_columns) -%} {{ dbtvault.prepend_generated_by() }} @@ -23,7 +27,7 @@ source_model: source_name: source_table_name" {%- endset -%} - + {{- exceptions.raise_compiler_error(error_message) -}} {%- endif -%} @@ -34,73 +38,103 @@ {%- set source_table_name = source_model[source_name] -%} {%- set source_relation = source(source_name, source_table_name) -%} - + {%- set all_source_columns = dbtvault.source_columns(source_relation=source_relation) -%} {%- elif source_model is not mapping and source_model is not none -%} {%- set source_relation = ref(source_model) -%} + {%- set all_source_columns = dbtvault.source_columns(source_relation=source_relation) -%} +{%- else -%} + + {%- set all_source_columns = [] -%} {%- endif -%} -{#- CTE to add source columns from the source model -#} -WITH stage AS ( - SELECT +{%- set derived_column_names = dbtvault.extract_column_names(derived_columns) -%} +{%- set hashed_column_names = dbtvault.extract_column_names(hashed_columns) -%} +{%- set ranked_column_names = dbtvault.extract_column_names(ranked_columns) -%} +{%- set exclude_column_names = derived_column_names + hashed_column_names %} +{%- set source_and_derived_column_names = all_source_columns + derived_column_names %} + +{%- set source_columns_to_select = dbtvault.process_columns_to_select(all_source_columns, exclude_column_names) -%} +{%- set derived_columns_to_select = dbtvault.process_columns_to_select(source_and_derived_column_names, hashed_column_names) | unique | list -%} +{%- set final_columns_to_select = [] -%} + +{#- Include source columns in final column selection if true -#} +{%- if include_source_columns -%} + {%- if dbtvault.is_nothing(derived_columns) + and dbtvault.is_nothing(hashed_columns) + and dbtvault.is_nothing(ranked_columns) -%} + {%- set final_columns_to_select = final_columns_to_select + all_source_columns -%} + {%- else -%} + {#- Only include non-overriden columns if not just source columns -#} + {%- set final_columns_to_select = final_columns_to_select + source_columns_to_select -%} + {%- endif -%} +{%- endif %} -{% if source_relation is defined -%} - {%- set included_source_columns = dbtvault.source_columns(source_relation=source_relation) -%} +WITH source_data AS ( - {%- for col in included_source_columns -%} - {{ ' ' ~ col }} - {{- ',\n' if not loop.last -}} - {%- endfor -%} + SELECT -{%- endif %} + {{- "\n\n " ~ dbtvault.print_list(all_source_columns) if all_source_columns else " *" }} FROM {{ source_relation }} -), - -{# Derive additional columns, if provided, and carry over source columns from previous CTE for use in the hash stage -#} -derived_columns AS ( - SElECT + {%- set last_cte = "source_data" %} +) - {%- if derived_columns is defined and derived_columns is not none -%} - {%- if include_source_columns or hashed_columns is defined and hashed_columns is not none %} +{%- if dbtvault.is_something(derived_columns) -%}, - {{ dbtvault.derive_columns(source_relation=source_relation, columns=derived_columns) | indent(width=4, first=false) }} - {%- else %} +derived_columns AS ( - {{ dbtvault.derive_columns(columns=derived_columns) | indent(4) }} + SELECT - {%- endif -%} + {{ dbtvault.derive_columns(source_relation=source_relation, columns=derived_columns) | indent(4) }} - {#- If source relation is defined but derived_columns is not -#} - {%- else -%} - {{ " *" }} - {%- endif %} + FROM {{ last_cte }} + {%- set last_cte = "derived_columns" -%} + {%- set final_columns_to_select = final_columns_to_select + derived_column_names %} +) +{%- endif -%} - FROM stage -), +{% if dbtvault.is_something(hashed_columns) -%}, -{# Hash columns, if provided, and process exclusion flags if provided -#} hashed_columns AS ( + SELECT - {%- if hashed_columns is defined and hashed_columns is not none %} - {{- " *," if include_source_columns -}} + {{ dbtvault.print_list(derived_columns_to_select) }}, + + {% set processed_hash_columns = dbtvault.process_hash_column_excludes(hashed_columns, all_source_columns) -%} + {{- dbtvault.hash_columns(columns=processed_hash_columns) | indent(4) }} + + FROM {{ last_cte }} + {%- set last_cte = "hashed_columns" -%} + {%- set final_columns_to_select = final_columns_to_select + hashed_column_names %} +) +{%- endif -%} + +{% if dbtvault.is_something(ranked_columns) -%}, + +ranked_columns AS ( - {%- if derived_columns is defined and derived_columns is not none and include_source_columns is false %} + SELECT *, - {{ dbtvault.derive_columns(columns=derived_columns) | indent(4) }}, - {%- endif %} + {{ dbtvault.rank_columns(columns=ranked_columns) | indent(4) if dbtvault.is_something(ranked_columns) }} - {%- set hashed_columns = dbtvault.process_excludes(source_relation=source_relation, derived_columns=derived_columns, columns=hashed_columns) %} + FROM {{ last_cte }} + {%- set last_cte = "ranked_columns" -%} + {%- set final_columns_to_select = final_columns_to_select + ranked_column_names %} +) +{%- endif -%} - {{ dbtvault.hash_columns(columns=hashed_columns) | indent(4) }} +, + +columns_to_select AS ( + + SELECT - {%- else -%} - {{ " *" }} - {%- endif %} + {{ dbtvault.print_list(final_columns_to_select) }} - FROM derived_columns + FROM {{ last_cte }} ) -SELECT * FROM hashed_columns +SELECT * FROM columns_to_select {%- endmacro -%} \ No newline at end of file diff --git a/macros/supporting/hash.sql b/macros/supporting/hash.sql index fe9d8f8fd..ffc485e2f 100644 --- a/macros/supporting/hash.sql +++ b/macros/supporting/hash.sql @@ -4,12 +4,15 @@ {%- set is_hashdiff = false -%} {% endif %} - {{- adapter.dispatch('hash', packages = var('adapter_packages', ['dbtvault']))(columns=columns, alias=alias, is_hashdiff=is_hashdiff) -}} + {{- adapter.dispatch('hash', packages = dbtvault.get_dbtvault_namespaces())(columns=columns, alias=alias, is_hashdiff=is_hashdiff) -}} {%- endmacro %} {%- macro default__hash(columns, alias, is_hashdiff) -%} +{%- set concat_string = "||" -%} +{%- set null_placeholder_string = "^^" -%} + {%- set hash = var('hash', 'MD5') -%} {#- Select hashing algorithm -#} @@ -27,33 +30,48 @@ {%- set standardise = "NULLIF(UPPER(TRIM(CAST([EXPRESSION] AS VARCHAR))), '')" %} {#- Alpha sort columns before hashing if a hashdiff -#} -{%- if is_hashdiff and columns is iterable and columns is not string -%} +{%- if is_hashdiff and dbtvault.is_list(columns) -%} {%- set columns = columns|sort -%} {%- endif -%} {#- If single column to hash -#} {%- if columns is string -%} {%- set column_str = dbtvault.as_constant(columns) -%} - CAST(({{ hash_alg }}({{ standardise | replace('[EXPRESSION]', column_str) }})) AS BINARY({{ hash_size }})) AS {{ alias }} + {{- "CAST(({}({})) AS BINARY({})) AS {}".format(hash_alg, standardise | replace('[EXPRESSION]', column_str), hash_size, alias) | indent(4) -}} {#- Else a list of columns to hash -#} {%- else -%} + {%- set all_null = [] -%} -CAST({{ hash_alg }}(CONCAT( + {%- if is_hashdiff -%} + {{- "CAST({}(CONCAT_WS('{}',".format(hash_alg, concat_string) | indent(4) -}} + {%- else -%} + {{- "CAST({}(NULLIF(CONCAT_WS('{}',".format(hash_alg, concat_string) | indent(4) -}} + {%- endif -%} -{%- for column in columns %} + {%- for column in columns -%} -{%- set column_str = dbtvault.as_constant(column) -%} + {%- do all_null.append(null_placeholder_string) -%} -{%- if not loop.last %} - IFNULL({{ standardise | replace('[EXPRESSION]', column_str) }}, '^^'), '||', -{%- else %} - IFNULL({{ standardise | replace('[EXPRESSION]', column_str) }}, '^^') )) -AS BINARY({{ hash_size }})) AS {{ alias }} -{%- endif -%} + {%- set column_str = dbtvault.as_constant(column) -%} + {{- "\nIFNULL({}, '{}')".format(standardise | replace('[EXPRESSION]', column_str), null_placeholder_string) | indent(4) -}} + {{- "," if not loop.last -}} -{%- endfor -%} -{%- endif -%} + {%- if loop.last -%} + + {% if is_hashdiff %} + {{- "\n)) AS BINARY({})) AS {}".format(hash_size, alias) -}} + {%- else -%} + {{- "\n), '{}')) AS BINARY({})) AS {}".format(all_null | join(""), hash_size, alias) -}} + {%- endif -%} + {%- else -%} -{%- endmacro -%} + {%- do all_null.append(concat_string) -%} + + {%- endif -%} + + {%- endfor -%} + +{%- endif -%} +{%- endmacro -%} \ No newline at end of file diff --git a/macros/supporting/prefix.sql b/macros/supporting/prefix.sql index 51f0c9c8e..d72bae973 100644 --- a/macros/supporting/prefix.sql +++ b/macros/supporting/prefix.sql @@ -1,6 +1,8 @@ {%- macro prefix(columns, prefix_str, alias_target) -%} - {{- adapter.dispatch('prefix', packages = var('adapter_packages', ['dbtvault']))(columns=columns, prefix_str=prefix_str, alias_target=alias_target) -}} + {{- adapter.dispatch('prefix', packages = dbtvault.get_dbtvault_namespaces())(columns=columns, + prefix_str=prefix_str, + alias_target=alias_target) -}} {%- endmacro -%} diff --git a/macros/tables/eff_sat.sql b/macros/tables/eff_sat.sql index 70495b920..cd45a8d53 100644 --- a/macros/tables/eff_sat.sql +++ b/macros/tables/eff_sat.sql @@ -1,9 +1,9 @@ {%- macro eff_sat(src_pk, src_dfk, src_sfk, src_start_date, src_end_date, src_eff, src_ldts, src_source, source_model) -%} - {{- adapter.dispatch('eff_sat', packages = var('adapter_packages', ['dbtvault']))(src_pk=src_pk, src_dfk=src_dfk, src_sfk=src_sfk, - src_start_date=src_start_date, src_end_date=src_end_date, - src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, - source_model=source_model) -}} + {{- adapter.dispatch('eff_sat', packages = dbtvault.get_dbtvault_namespaces())(src_pk=src_pk, src_dfk=src_dfk, src_sfk=src_sfk, + src_start_date=src_start_date, src_end_date=src_end_date, + src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, + source_model=source_model) -}} {%- endmacro -%} {%- macro default__eff_sat(src_pk, src_dfk, src_sfk, src_start_date, src_end_date, src_eff, src_ldts, src_source, source_model) -%} @@ -20,14 +20,26 @@ WITH source_data AS ( {%- if model.config.materialized == 'vault_insert_by_period' %} WHERE __PERIOD_FILTER__ {% endif %} + {%- set source_cte = "source_data" %} ), + +{%- if model.config.materialized == 'vault_insert_by_rank' %} +rank_col AS ( + SELECT * FROM source_data + WHERE __RANK_FILTER__ + {%- set source_cte = "rank_col" %} +), +{% endif -%} + {%- if load_relation(this) is none %} + records_to_insert AS ( SELECT {{ dbtvault.alias_all(source_cols, 'e') }} - FROM source_data AS e + FROM {{ source_cte }} AS e ) {%- else %} -latest_eff AS + +latest_open_eff AS ( SELECT {{ dbtvault.alias_all(source_cols, 'b') }}, ROW_NUMBER() OVER ( @@ -35,19 +47,16 @@ latest_eff AS ORDER BY b.{{ src_ldts }} DESC ) AS row_number FROM {{ this }} AS b + WHERE TO_DATE(b.{{ src_end_date }}) = TO_DATE('9999-12-31') + QUALIFY row_number = 1 ), -latest_open_eff AS -( - SELECT {{ dbtvault.alias_all(source_cols, 'a') }} - FROM latest_eff AS a - WHERE TO_DATE(a.{{ src_end_date }}) = TO_DATE('9999-12-31') - AND a.row_number = 1 -), + stage_slice AS ( SELECT {{ dbtvault.alias_all(source_cols, 'stage') }} - FROM source_data AS stage + FROM {{ "rank_col" if model.config.materialized == 'vault_insert_by_rank' else "source_data" }} AS stage ), + new_open_records AS ( SELECT DISTINCT {{ dbtvault.alias_all(source_cols, 'stage') }} @@ -59,6 +68,7 @@ new_open_records AS ( AND {{ dbtvault.multikey(src_sfk, prefix='stage', condition='IS NOT NULL') }} ), {%- if is_auto_end_dating %} + links_to_end_date AS ( SELECT a.* FROM latest_open_eff AS a @@ -67,6 +77,7 @@ links_to_end_date AS ( WHERE {{ dbtvault.multikey(src_sfk, prefix='b', condition='IS NULL', operator='OR') }} OR {{ dbtvault.multikey(src_sfk, prefix=['a', 'b'], condition='<>', operator='OR') }} ), + new_end_dated_records AS ( SELECT DISTINCT h.{{ src_pk }}, @@ -76,6 +87,7 @@ new_end_dated_records AS ( INNER JOIN links_to_end_date AS g ON g.{{ src_pk }} = h.{{ src_pk }} ), + amended_end_dated_records AS ( SELECT DISTINCT a.{{ src_pk }}, @@ -90,6 +102,7 @@ amended_end_dated_records AS ( AND {{ dbtvault.multikey(src_dfk, prefix='stage', condition='IS NOT NULL') }} ), {%- endif %} + records_to_insert AS ( SELECT * FROM new_open_records {%- if is_auto_end_dating %} diff --git a/macros/tables/hub.sql b/macros/tables/hub.sql index 8305f612b..241ec3b34 100644 --- a/macros/tables/hub.sql +++ b/macros/tables/hub.sql @@ -1,8 +1,8 @@ {%- macro hub(src_pk, src_nk, src_ldts, src_source, source_model) -%} - {{- adapter.dispatch('hub', packages = var('adapter_packages', ['dbtvault']))(src_pk=src_pk, src_nk=src_nk, - src_ldts=src_ldts, src_source=src_source, - source_model=source_model) -}} + {{- adapter.dispatch('hub', packages = dbtvault.get_dbtvault_namespaces())(src_pk=src_pk, src_nk=src_nk, + src_ldts=src_ldts, src_source=src_source, + source_model=source_model) -}} {%- endmacro -%} @@ -10,6 +10,10 @@ {%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_nk, src_ldts, src_source]) -%} +{%- if model.config.materialized == 'vault_insert_by_rank' %} + {%- set source_cols_with_rank = source_cols + [config.get('rank_column')] -%} +{%- endif -%} + {{ dbtvault.prepend_generated_by() }} {{ 'WITH ' -}} @@ -18,63 +22,73 @@ {%- set source_model = [source_model] -%} {%- endif -%} +{%- set ns = namespace(last_cte= "") -%} + {%- for src in source_model -%} {%- set source_number = loop.index | string -%} -rank_{{ source_number }} AS ( +row_rank_{{ source_number }} AS ( + {%- if model.config.materialized == 'vault_insert_by_rank' %} + SELECT {{ source_cols_with_rank | join(', ') }}, + {%- else %} SELECT {{ source_cols | join(', ') }}, + {%- endif %} ROW_NUMBER() OVER( PARTITION BY {{ src_pk }} ORDER BY {{ src_ldts }} ASC ) AS row_number FROM {{ ref(src) }} -), -stage_{{ source_number }} AS ( - SELECT DISTINCT {{ source_cols | join(', ') }} - FROM rank_{{ source_number }} - WHERE row_number = 1 -), + QUALIFY row_number = 1 + {%- set ns.last_cte = "row_rank_{}".format(source_number) %} +),{{ "\n" if not loop.last }} {% endfor -%} - +{% if source_model | length > 1 %} stage_union AS ( {%- for src in source_model %} - SELECT * FROM stage_{{ loop.index | string }} + SELECT * FROM row_rank_{{ loop.index | string }} {%- if not loop.last %} UNION ALL {%- endif %} {%- endfor %} + {%- set ns.last_cte = "stage_union" %} ), +{%- endif -%} {%- if model.config.materialized == 'vault_insert_by_period' %} -stage_period_filter AS ( +stage_mat_filter AS ( SELECT * - FROM stage_union + FROM {{ ns.last_cte }} WHERE __PERIOD_FILTER__ + {%- set ns.last_cte = "stage_mat_filter" %} ), -{%- endif %} -rank_union AS ( +{%- elif model.config.materialized == 'vault_insert_by_rank' %} +stage_mat_filter AS ( + SELECT * + FROM {{ ns.last_cte }} + WHERE __RANK_FILTER__ + {%- set ns.last_cte = "stage_mat_filter" %} +), +{%- endif -%} +{%- if source_model | length > 1 %} + +row_rank_union AS ( SELECT *, ROW_NUMBER() OVER( PARTITION BY {{ src_pk }} ORDER BY {{ src_ldts }}, {{ src_source }} ASC - ) AS row_number - {%- if model.config.materialized == 'vault_insert_by_period' %} - FROM stage_period_filter - {%- else %} - FROM stage_union - {%- endif %} + ) AS row_rank_number + FROM {{ ns.last_cte }} WHERE {{ src_pk }} IS NOT NULL + QUALIFY row_rank_number = 1 + {%- set ns.last_cte = "row_rank_union" %} ), -stage AS ( - SELECT DISTINCT {{ source_cols | join(', ') }} - FROM rank_union - WHERE row_number = 1 -), +{% endif %} records_to_insert AS ( - SELECT stage.* FROM stage + SELECT {{ dbtvault.prefix(source_cols, 'a', alias_target='target') }} + FROM {{ ns.last_cte }} AS a {%- if dbtvault.is_vault_insert_by_period() or is_incremental() %} LEFT JOIN {{ this }} AS d - ON stage.{{ src_pk }} = d.{{ src_pk }} + ON a.{{ src_pk }} = d.{{ src_pk }} WHERE {{ dbtvault.prefix([src_pk], 'd') }} IS NULL {%- endif %} ) diff --git a/macros/tables/link.sql b/macros/tables/link.sql index 3407a4e7c..a7368f1c6 100644 --- a/macros/tables/link.sql +++ b/macros/tables/link.sql @@ -1,8 +1,8 @@ {%- macro link(src_pk, src_fk, src_ldts, src_source, source_model) -%} - {{- adapter.dispatch('link', packages = var('adapter_packages', ['dbtvault']))(src_pk=src_pk, src_fk=src_fk, - src_ldts=src_ldts, src_source=src_source, - source_model=source_model) -}} + {{- adapter.dispatch('link', packages = dbtvault.get_dbtvault_namespaces())(src_pk=src_pk, src_fk=src_fk, + src_ldts=src_ldts, src_source=src_source, + source_model=source_model) -}} {%- endmacro -%} @@ -11,6 +11,10 @@ {%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_fk, src_ldts, src_source]) -%} {%- set fk_cols = dbtvault.expand_column_list([src_fk]) -%} +{%- if model.config.materialized == 'vault_insert_by_rank' %} + {%- set source_cols_with_rank = source_cols + [config.get('rank_column')] -%} +{%- endif -%} + {{ dbtvault.prepend_generated_by() }} {{ 'WITH ' -}} @@ -19,67 +23,79 @@ {%- set source_model = [source_model] -%} {%- endif -%} +{%- set ns = namespace(last_cte= "") -%} + {%- for src in source_model -%} -{%- set source_number = (loop.index | string) -%} +{%- set source_number = loop.index | string -%} -rank_{{ source_number }} AS ( +row_rank_{{ source_number }} AS ( + {%- if model.config.materialized == 'vault_insert_by_rank' %} + SELECT {{ source_cols_with_rank | join(', ') }}, + {%- else %} SELECT {{ source_cols | join(', ') }}, + {%- endif %} ROW_NUMBER() OVER( PARTITION BY {{ src_pk }} ORDER BY {{ src_ldts }} ASC ) AS row_number FROM {{ ref(src) }} -), -stage_{{ source_number }} AS ( - SELECT DISTINCT {{ source_cols | join(', ') }} - FROM rank_{{ source_number }} - WHERE row_number = 1 -), + QUALIFY row_number = 1 + {%- set ns.last_cte = "row_rank_{}".format(source_number) %} +),{{ "\n" if not loop.last }} {% endfor -%} - +{% if source_model | length > 1 %} stage_union AS ( {%- for src in source_model %} - SELECT * FROM stage_{{ loop.index | string }} + SELECT * FROM row_rank_{{ loop.index | string }} {%- if not loop.last %} UNION ALL {%- endif %} {%- endfor %} + {%- set ns.last_cte = "stage_union" %} ), +{%- endif -%} {%- if model.config.materialized == 'vault_insert_by_period' %} -stage_period_filter AS ( +stage_mat_filter AS ( SELECT * - FROM stage_union + FROM {{ ns.last_cte }} WHERE __PERIOD_FILTER__ + {%- set ns.last_cte = "stage_mat_filter" %} ), -{%- endif %} -rank_union AS ( +{%- elif model.config.materialized == 'vault_insert_by_rank' %} +stage_mat_filter AS ( + SELECT * + FROM {{ ns.last_cte }} + WHERE __RANK_FILTER__ + {%- set ns.last_cte = "stage_mat_filter" %} +), +{% endif %} +{%- if source_model | length > 1 %} + +row_rank_union AS ( SELECT *, ROW_NUMBER() OVER( PARTITION BY {{ src_pk }} ORDER BY {{ src_ldts }}, {{ src_source }} ASC - ) AS row_number - {%- if model.config.materialized == 'vault_insert_by_period' %} - FROM stage_period_filter - {%- else %} - FROM stage_union - {%- endif %} + ) AS row_rank_number + FROM {{ ns.last_cte }} WHERE {{ dbtvault.multikey(fk_cols, condition='IS NOT NULL') }} + QUALIFY row_rank_number = 1 + {%- set ns.last_cte = "row_rank_union" %} ), -stage AS ( - SELECT DISTINCT {{ source_cols | join(', ') }} - FROM rank_union - WHERE row_number = 1 -), +{% endif %} records_to_insert AS ( - SELECT stage.* FROM stage + SELECT {{ dbtvault.prefix(source_cols, 'a', alias_target='target') }} + FROM {{ ns.last_cte }} AS a {%- if dbtvault.is_vault_insert_by_period() or is_incremental() %} LEFT JOIN {{ this }} AS d - ON stage.{{ src_pk }} = d.{{ src_pk }} + ON a.{{ src_pk }} = d.{{ src_pk }} WHERE {{ dbtvault.prefix([src_pk], 'd') }} IS NULL {%- endif %} ) SELECT * FROM records_to_insert +{%- endmacro -%} + {%- endmacro -%} \ No newline at end of file diff --git a/macros/tables/sat.sql b/macros/tables/sat.sql index d26b4e922..589ed5514 100644 --- a/macros/tables/sat.sql +++ b/macros/tables/sat.sql @@ -1,25 +1,44 @@ {%- macro sat(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, source_model) -%} - {{- adapter.dispatch('sat', packages = var('adapter_packages', ['dbtvault']))(src_pk=src_pk, src_hashdiff=src_hashdiff, - src_payload=src_payload, src_eff=src_eff, src_ldts=src_ldts, - src_source=src_source, source_model=source_model) -}} + {{- adapter.dispatch('sat', packages = dbtvault.get_dbtvault_namespaces())(src_pk=src_pk, src_hashdiff=src_hashdiff, + src_payload=src_payload, src_eff=src_eff, src_ldts=src_ldts, + src_source=src_source, source_model=source_model) -}} {%- endmacro %} {%- macro default__sat(src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source, source_model) -%} {%- set source_cols = dbtvault.expand_column_list(columns=[src_pk, src_hashdiff, src_payload, src_eff, src_ldts, src_source]) -%} +{%- set rank_cols = dbtvault.expand_column_list(columns=[src_pk, src_hashdiff, src_ldts]) -%} + +{%- if model.config.materialized == 'vault_insert_by_rank' %} + {%- set source_cols_with_rank = source_cols + [config.get('rank_column')] -%} +{%- endif -%} {{ dbtvault.prepend_generated_by() }} WITH source_data AS ( - SELECT * - FROM {{ ref(source_model) }} + {%- if model.config.materialized == 'vault_insert_by_rank' %} + SELECT {{ dbtvault.prefix(source_cols_with_rank, 'a', alias_target='source') }} + {%- else %} + SELECT {{ dbtvault.prefix(source_cols, 'a', alias_target='source') }} + {%- endif %} + FROM {{ ref(source_model) }} AS a {%- if model.config.materialized == 'vault_insert_by_period' %} WHERE __PERIOD_FILTER__ {% endif %} + {%- set source_cte = "source_data" %} +), + +{%- if model.config.materialized == 'vault_insert_by_rank' %} +rank_col AS ( + SELECT * FROM source_data + WHERE __RANK_FILTER__ + {%- set source_cte = "rank_col" %} ), -{% if dbtvault.is_vault_insert_by_period() or is_incremental() -%} +{% endif -%} + +{% if dbtvault.is_vault_insert_by_period() or dbtvault.is_vault_insert_by_rank() or is_incremental() %} update_records AS ( SELECT {{ dbtvault.prefix(source_cols, 'a', alias_target='target') }} @@ -27,29 +46,26 @@ update_records AS ( JOIN source_data as b ON a.{{ src_pk }} = b.{{ src_pk }} ), -rank AS ( - SELECT {{ dbtvault.prefix(source_cols, 'c', alias_target='target') }}, + +latest_records AS ( + SELECT {{ dbtvault.prefix(rank_cols, 'c', alias_target='target') }}, CASE WHEN RANK() OVER (PARTITION BY {{ dbtvault.prefix([src_pk], 'c') }} ORDER BY {{ dbtvault.prefix([src_ldts], 'c') }} DESC) = 1 THEN 'Y' ELSE 'N' END AS latest FROM update_records as c + QUALIFY latest = 'Y' ), -stage AS ( - SELECT {{ dbtvault.prefix(source_cols, 'd', alias_target='target') }} - FROM rank AS d - WHERE d.latest = 'Y' -), -{% endif -%} +{%- endif %} records_to_insert AS ( SELECT DISTINCT {{ dbtvault.alias_all(source_cols, 'e') }} - FROM source_data AS e - {% if dbtvault.is_vault_insert_by_period() or is_incremental() -%} - LEFT JOIN stage - ON {{ dbtvault.prefix([src_hashdiff], 'stage', alias_target='target') }} = {{ dbtvault.prefix([src_hashdiff], 'e') }} - WHERE {{ dbtvault.prefix([src_hashdiff], 'stage', alias_target='target') }} IS NULL - {% endif %} + FROM {{ source_cte }} AS e + {%- if dbtvault.is_vault_insert_by_period() or dbtvault.is_vault_insert_by_rank() or is_incremental() %} + LEFT JOIN latest_records + ON {{ dbtvault.prefix([src_hashdiff], 'latest_records', alias_target='target') }} = {{ dbtvault.prefix([src_hashdiff], 'e') }} + WHERE {{ dbtvault.prefix([src_hashdiff], 'latest_records', alias_target='target') }} IS NULL + {%- endif %} ) SELECT * FROM records_to_insert diff --git a/macros/tables/t_link.sql b/macros/tables/t_link.sql index a00c5406e..638724923 100644 --- a/macros/tables/t_link.sql +++ b/macros/tables/t_link.sql @@ -1,8 +1,8 @@ {%- macro t_link(src_pk, src_fk, src_payload, src_eff, src_ldts, src_source, source_model) -%} - {{- adapter.dispatch('t_link', packages = var('adapter_packages', ['dbtvault']))(src_pk=src_pk, src_fk=src_fk, src_payload=src_payload, - src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, - source_model=source_model) -}} + {{- adapter.dispatch('t_link', packages = dbtvault.get_dbtvault_namespaces())(src_pk=src_pk, src_fk=src_fk, src_payload=src_payload, + src_eff=src_eff, src_ldts=src_ldts, src_source=src_source, + source_model=source_model) -}} {%- endmacro %} @@ -18,6 +18,9 @@ WITH stage AS ( {%- if model.config.materialized == 'vault_insert_by_period' %} WHERE __PERIOD_FILTER__ {%- endif %} + {%- if model.config.materialized == 'vault_insert_by_rank' %} + WHERE __RANK_FILTER__ + {%- endif %} ), records_to_insert AS ( SELECT DISTINCT {{ dbtvault.prefix(source_cols, 'stg') }}

j2(U1=R{ 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/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�tpKb09d{CABtM?O%&Ckf^XQ4 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⪙+u&H{uT<1)Nx1V*X!6*e_qJNpqYIej+-F=kdc0M(@*AGmrt0rtw znXN%7;eC`Sz-6y`%f`haP{_WNgE(vEgwfJc7F-?`Kct`&pF5FeY8{Z=+rwn+u;2AT z)nhi8&7CaH8-7uJ61K(L-k&9w5a6of4vkw)oCM}@YfE^WLnMBc;{S!Yw2+=%F@JlQ zBru~mRQnO3_4F&(wAj9O5j%pz7&*e4;R=J|tddN868*NWmBV<}UZs%UVR4lIJD5R$ z$2pu?{+GL?nCMk|CKcwXXuCf0t=+`+?OSE##ZA3hVcRb3Tw_`$oFoTo>Dl6;;Z95r z1O2Y;#ayC;K;HyhMdMC;u**?Wmr@Y}&Kdd-5i`z$0cp~{OTZyG8K^YhlWIfzeNh)F zL5!jv&pA5hogd?oeb=GAcKQ`#&`frEwYsriO)e^j$tn#!xG;m7b|9#iKt%Z*MWyj> z4b_SGvsD(q8~gRO!BTS3QoJTN7Ml8≪|f5Q}c`LX;$xQgf?i3^QH_NC9v9NW+BC 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%0Au(1Ocra7esypi+pj)Q~u2q_>>Y0)w%81L1J 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≺l%Ny7_UJb5% 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

j2(U1=R{ zK2bavx{7og{kshtnbLRL?XbrPr}-8xnpeZ;bgws#_g;G(=$b_I2c{K0i|co~A*>VJ zGuFx8)M)Dt$cgLe!M!e`mbs<0T>0BKhbikHaSXtNCOx*4U9u`)%geOLn{Pn-RaUew zZ3j{!S5h-VTu}3JC{K%d?IAka{($gcAiB_eQwuw{kQ6o33l9+8cZWPJDlg`o@wwwQ zFh#GPr=t?xS|!Q%90&;bf^9fzch!zPFsXJYsqq~CqSW}T(iqCj!xK*2K+(L+3=w!Q z=|g4oLM(dtPx}I%Sz-K#T7{ID?I296s@l-SuA^9OYizx)YTqHBlvu zc~{YRRkqtA?b0wXFvdcep`p<2Hqhb#^N(V{WUgXDN{rw`9ea98F2{Wd=KSPi}MC`C_Mo0i;HQj$62cMlYwa_@s} zI~rz7zhnKGY`8%2XnI}KwxZ`SF(YT@+)cjEZBn~5hI3?0Q}OF=gghR3eaTkE3iA}u zMisk@D4>?mopEq_I&HY30*J>ZQwrP1um>R28R=U`ctnJB#|K6YK)bAcaKU?bIDwH0 z3iTTpC@G-+Nx(ay4a{ygFy!Fy#dXft+B`U|m+Hugb_sKWeQ3e7Tz64l-`F`gmXZum zoFfq+U9SpUuk;sHgxN+P9#m*DwO_8~JyhiVMP8eskCSNl8(uza^*%{(FourE(|cP; zwSE+0Fb|HRNf%a)h;Ama=5BYDWQDUxlpB$yciLqot0G^RFH@eZWt626ZTd)N2s+VC|RO;7Z;1}05k1h$lm|+URjQN~QWmRr<6>edSVW;wehgXO??u5{2Yc6Ii;H(E< zMyKo;EYXrYx?q&1qI3S2ZR2gFzex$TusG};0tS;fWpI~Cd2v|BZjYV z`lX(#%hk$9W!syXZ{HR5QK75RbcEL$=rmHsPu8&P_@P>fvyEz8Zh5tajpR($Y(e7{7FwVkrOgty0~WK-D=_R?V_&ePBJ9_VwDmwT=+p zW6TV?%FKW~=-cVeF|h*Lfw>n%wOzZ4wj=$21IK^TfZOUmSWbUJ%BQE*0x;Vf7bR;R!7x@&ZdWx`uFOv8(+SM|!A=fH0UQ z2_1uRLnO7gxU4HQYI_jhNoWUDJ^HCEteL%W$pwfF=y&5{D2q8S(Bhm!{96gRN8^C) ze*+yvRM#DaNYXgZvT zg^6^6g382$E;9Kk`^?rTkA)!)98nxDNYIP2=~wLUkoLyyZZP+y%UF^HlmrM{U`A$s zlj>oN>VbXZ2n-`3W__{zXl~tW1gpljC5nYGXERV3pPlBHq+^Bsm*fP-*R8B6oPN1b zT29qd#*D;YGNCR1I$_FpF)L*vmuks$da`l$=T;w1@>KqfRj#E#Fmw1B0WYi;!{@9} z%$sW50@aV0OvkobXE*f7k^Y8CA&>Ew?CFK<G1jl)`)+TL^Uo@!Y&|_{D)rcE6xDf zOg;r6wG*nZW7N67p)#!5@3}v7;fbd>k*Wr8VZSbgM!O$)r7yLO~Xl4j>qQ?20lHS%bws$coy!Y7(q`CpZCR;Oiwud{5k$50zRK^pbpqV zA=s{dI)p}1ajw3PxPK896bnI;UTwsBmflxsK2y|C!;e&#sPdPuGBo0PYx6phL@bkq zNX`1uBt=Kgn81Q>96`1qMhVTHF^rvP_RJ+}?p;TgpA_FCXSCz!td)bhxbJ+)@#{q> zL?;4*(2O=q(5=;69ghKyeJ*KAu@=;@EV(8e{ZDT%KcuYxR3vii{d1P>ToaOnvAmSe zO=4kVz+z0BGg@Uy^LQM~rFwv?+@Dt0?-ozxVy_Dv;S9%01FvXdqeze!3Z_juQ=yq7 z$6tUsF=L^GCzMdISB<1yo{S zb1FLoYuep4aq2QR#HTQ~VX2NB z=xizK+#C9>+l@B!l}JMwQX%03bAe0CHNQpV6j@XsODatDoK2APY0Vz-cJIrmA!Ano z%~i@m>Ve5;OEv5(*CB1<3(Lh4CZo@ z6ODbY=+OR(5f+QI^$RLfgLC7}Z@1 ziMzqO(}|nUPl@Ltz!9e2C6gT2$2W8-txayD>i0>p5*6nHpcGxnY>JN)R_A^4KQpz~ zHWdWznY}d0rF=M7FFX#dQ!?#uzvP`1h@cd)H-}w?^mAOC6W4S!Iq{T@SK>I7TveOZ zDH-9XP#=L3o19Se%Ir5tJd6yt!n!+iCJac{!wbKa!oimfZ`@c9b!hWV?Xh{jlxku^ z6NAc2;4$#PCJant4JV&)P{^dK>XeoWgEEIAIZUJH<>2g4a3TTc`ucifIDv*Ki#ybEWOS=n6`!S2 zPU&*zdXzx02M$)B(38!b3|&>DBM7^H$9lwl2NIu<%|5u6S{LBTsuB&cJ(W4AJbYkL z9Un8uG20{BerV~GXtFt3c%yM|xn^rOSg(>8b%Dhug?$`LcwZRV)S8At<{+vLjo6GG ziT;r?#7MdtIsLV92h!B3Sqem|n zwHWOjXHBqc}3Yv528-gm@Pi;40KbSU?+{KWIm>H6AX@7aTOeVaBnou&mow0E4UB{n>7eJgw>91 zbQ(=C-xnkR*s1L&_>$D8w-{=uQg4BwRHhPmqh2GFgp=Kz*7k+ZJRz4JKCDtTVy?7r z0zkWt(`i*u!l+%h$}rO4N@*OHEIQ7yu~1-fLu=2Fl}-s3bVTuwWovp;l|yK-1H(5F zsWu?ee&s$I(WJTlQ1ib!$5+pq?VP7p8w4XY9u9;YH3bdo#d1&!LxrgSigGgC`A9^l zhfdphWXWD!W**poFCo5tz9^bxQ;{MqmcdKwEt6H;n>h>qo1i(Y=b3Dtqb2$=ZCsQc zJV_c~7S^4AsGp0UU9H4&G-2A-PKsrLwX%z0Ks{5^FbrgRc4JjM^eWk^J2+5f#+MlFVUnSTA@jYOhaTh#e*)*G=3jpnB){mSxGlONT9Na@ z8TC%Ub!pDf4-u`ozp&vNFi6f^Mbh6>wsCygfz6SPM9Eji@m_C34~z%*ME3nooNWoe z(L&t(op&5*^@>frMJh15_HN2^xi3&?{HgexHC}Q4-L91Rmyx2R0k5x3;NqnBN7Oo& zCW3e7@QrG}niE6?y?{*&T_HRVl5sA+F}1{PBga{YH0fuy-8gMGtkt{<>#$>&?M92C zTa%K@pr7uzNo#n@flGxvMJw@=R#QYYZIGyX-eLBOTuZwQw3S4Ewx#S*XQDjF-hl{E z-+;OzI(iq3o4JCjb!CfdE|`R%~+&wDo(0%qGH5Bh7>FzL*# zHY$@2zX`rki0Axa%}DocAoUw5?D;dL5Gi;9+avGjaN`ETPJmX59e|XdQtgyZ@g<%X zF&Pb%rA%fyjH3RI(wok?Xsh<|1q(eUh*$Ky69M88f{t$E`A`rS#`zRbe<%z?-~pqO z=$sb?=LH?toZ;UN|NRH4<3}ENQwx!~K}74zZP1?|sFYc%_9n1%+`tTKP=;R!4+|2k z3u*Pu?C~5>8cTU~LjV&e1>2#{t|-&)_;~dBARRJ*24r>UG#YtPC3LOcQ-y<_#QCd; zC{Ne~2os>Ud2Bv4RZsa=>cI*qy zm*}ln^JZ_y5*rCQ=@~jaBuXZ`{JnvSBz=a4BgbewvzO^#n_c$ z!sc?|I#G0185R4-Y{sW4g38GGZjy**qOGtk8_MN4GJJZF5OcX``wbKhVswf6 zoa@kh&MLEBk8irflb;TMe$YYb!?lf*Sb8iVXp`xvQJ6Gbta*MtI zemDJaip->I$nP8 z>p-ma3Pr%u*NZn;Q=#wRH_W)gGUGFBwy8~>b|ns70xM)|ysF-^sPg*`RmwO_Boqx_2CshC&UjMI=0 zcjQw*ToIyW{)8|KlYn}?zh4-Q?Lid-J09+03;H4BUES#YM4AJxjdjP+KXulF?r@y& zviyfV>g~5sNlTraoXmc#GB*dWZ9s_Zq3s~eJoN4>s%;n#ITM=K9I84go{l=Xm4S~m zu$gx}p<4sr_v{YDymqER#tVaeQ}eF5N^dD+d_ny$dIh7?ETX_NvQPl6 z8*#qHn?ZVJXbW-SYl1=i>F2lP%u71(%E zJ|1Q-6C$dAye;slysS{Qczu%ixghuY`Qe*KbS`g15Y0@@q~;&dPyDX&@w zgD7L;gnGjm^@F!#+B&$L00EsRShw+()WV%R*nV#UKb?ADV5XDn&Y}ojdA^8;AW6fN zH^Wgyu>Woji>)@yyy37Xs3|$Qn6xwi4m&R0$?70}{aIZMjm@!bQGfyadq{(&;E+4b zuIEOoAv2EcO)Ap@D(UUoiHugyX)H)!4-KSd9I~v9&`N{m*uM-{sYxYAqRE*>5_vbY zB1DBvyVbH!veNV$3M^AreAenN{et8oyN9{M53I9e&HDtaDU|kj);-*6QLK@ zAk+`v)kKvf(+0Eh4qj%Z7#PcrSX>;lm>vznI9eCd4@%>#evv(jF5HKQ#<&RUkEWvJ z+K-v+piCFM_9wq2WT*{j(%y%}Z@IsW1?nN^LgDAV>FJ_A_;+Prg&x)BoNU)f&^K4ey6{7~sj&?<@T+bVG)!F-59sLALIzkWW6wD=g|^yH^yrHL z=IGeAwcHVCo@xa&#Y$lGqfMEoz0;$5B4HU5eU^UQEI~28 zIO*jpQ3b~Zp59b0C*J)}L`(5PIA>kppFAlJ=iurZoT04ZywEK=1$nt?3hTRCsi3}P znHW(uy^xdAGtBzW{IFAy8YLUmAWT9_sX~^nDHx&Y)w+T_QmfQ4h^-w;>;0~cE_1RT zgsPtJ88~V}j&xW5IUCy$a#D{El1UKEKC4bK*l!k(Jwi3O897X~6T-tqo&-~j z79ED;>52Ccjs17&^_b8{T_8sC1*U=X*rW5(#j-a#%Q0E~%C>DTyw%Z3DQiHX&A9K^ z-KM)-n{l6t5WZwY$VsHtjwOior@=g|lf~PGt4?-nZwCRsHBd7UCWrV(mJDQH8xjN_ zWMhk~`G|f+3qggvdvSqF5ynt63xYR`I_~{zy$|rz$Qd=51GRl(7L34^oO^05PO4^s zRJ$0YBI^-EPh*Jud_zOh2XWQ{w$lcjz9RZ0=_xtskYkTMg^7Fvi=jb*6gFcBC9~z_ zZ!y187+IQ0KtT|L0WRt$x?VEi9g2!WXmLSAqf!g_Z75=ZBF1?AP=zg{+~&vmPDnLf zmvAvE_0jbCfx}dp95Pre5`P9X5eqD8cw~+3)+^zVM`kJ=03zx<`x|kanULJHlgGw< zuRw80i?4}+N1++5IA*9*nK2c*&KmPomB|G|@>Tq%vuTfR(pa~Ard!0J(GY$+1jZ0$ zmB|{HM$a^Dq+wk8C5)C-@hMl~QX^Ni6UvdWcL;DgmP|k?C#V<65HrS->+B9D4iRN2 zLe-;IO*@yY{8Rsi*C*AOXfi7B5|JX`utx2CrDn+v#P- zDG5NL5<T2>zb?pBC4c`sgr!U9u3gT(dQc!V^r1FZ@k2#j@B+S=XTa56gsQEvC#$yScV_ z{IqG!(LhY>zIoY>wcw7xhW~;$k+1M0guMf_{uSy(a!wB^aCBI*aYZi7W2~Ppr>Z;D z>NcqT8my>aX9W7SJ`WA8hk*8`PDu~?Dg>DCJqC=>U!thR6c%f+V8b{z+Yx>|r=DDk zjHE0XkCnH(DR^Qatj3qDzY4U~gp+A-($z5tSAC4kF50HLEus@2ycQT-(0tx9+m!S&jla zYoIkkR!67D=RWDA0Jr!ZzjFoOXy-k8%1O9yTSbMH$Z(NB2p=7k#Y$t$n>P&=)n}u6 zt$oM)Kf3MDfRsqzF;gx37H}>!bhY6irLUi-t%!|%(s1YGyxg)_)ZjhIb7V+^oY!;v zs)euV?EH>Mb&tf?vVI-NR~|E%WhKe$@z+zozO>wwH1l4jpHcrF;E$>1JkO0`sL+bx+jygj79Z} z&0S0XR73dX5=Gav-RH)^dg$7t%*@YnjVe|?hD)bCx7%ps-&ofR<=iXfBZ!RlE?MR> zyxf1v4i&WuZ_-)V8gq#1gTmA&~ax zi{-#%fp{z;h&B|NQa((ghaezpQd>ZhR0P$ql8*{!qx4I9H&(bDWYk`b2aP7i2`kQj zC7o^mTsuXIzyQbI7=Ki?VE!GZ^E78XY@=zdn-W_lwCs!lh)1=;m+_ioMfr710y?EK zuB=uvExsh}_Gh=7TpI$q1Td_dP;+r#mE4v+k^qx<_SwMNi0)Os)~IU?4eBB{H}?;q=>P0|bkP0kQ-j5Sg@WeZrFzT&#YuWkZ{_Zt&~TZy*oidyi|pY{5`o^6K@X zrxB8fyW8FSD8bAHs_$Vh-=kf1QH@Wouk#id%IOM@zvxtT1JrTe_ z6{H>()A~0^@fQ&AH$gBX;6pNA2+oKq@Hf!WNXTbB=OFsy4B*oE{{D6p1tgU%jr>x9 z=5N3tTO8oNn5)0z{rwG)yz?^xKulox$Or@1F8=vD!oNQR@Bn$iys!{Jh4?Ih4bBaj zE1<*xa6AGb-y>!3*MMGp(0|o<13(-wUI-Ci@k#pe~x3wt14%k=+Zspw%E1NVTN?0OE zsTy5b%h9pRYHbSpkUaI2wiQY)VblT@4%U8qT(|%rDk){ppI8KR#9$PZln);J6Ir~( z5AJC{uQgmTz3cn8snkeGNgs~NHJXqxSuB#v6${+P5CM*B6C%fcGrDYgUqG{fql6UQ z&dx4h=lwWA+{b=yM`N^UU2pvB+md-D?1)?R;m*AHmM%?adh}|rI5Fw30)5j%bWZ?EGqEC1be%F~IA5!DG$j(j@bw;4$c-?PKqD1DW+JYKz za&JrXf&SBq4d^A`KK`$kl_Li9B!Oy0F_OPpk$v(9)v58#{-Tn9;2|NrUI4XH4Q7T6 z`46J{uh#od-$eKdkWo}jOTPJk{Y+N`&`EpgR$e7s|7!38iSHno#Z0u0P^U9M3SDPl z6afFgyM~M|H9GhOB`)-L_eJn+k^t(rXEq|=W{e7uLgOWS`NRL%^S3E{!SDSS!4wk7 z|9|>Wrv?eIQ*YmN^YiTQQ~&^}`3^HRTM7d2oBtY|90xWS>LkO$shIk&-iI&#?v@%= zF|z-r`pY7|e^o7o1YoBaS3wp2VLw0E&EouwfVy&S zJH}%84<}^R8R6{)e;!rU5~P0dGKwO^AsSyf01!(_9xE5NAM1_0;Q?tdRP%kimhIay$z%Qq&|snw|l4)(kq-s-*|!UhNkD^yrJ zts^0>qg*kt!^df+)!}2yqGgX!9l>bGvhLIOMu^iuycT+IGJw_pxO8!Zxnx?|cFVDH zJ!Xx+=|rgax#F>=@)w*VI&Tj_gl(>Wyi|~F/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�tpKb09d{CABtM?O%&Ckf^XQ4 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⪙+u&H{uT<1)Nx1V*X!6*e_qJNpqYIej+-F=kdc0M(@*AGmrt0rtw znXN%7;eC`Sz-6y`%f`haP{_WNgE(vEgwfJc7F-?`Kct`&pF5FeY8{Z=+rwn+u;2AT z)nhi8&7CaH8-7uJ61K(L-k&9w5a6of4vkw)oCM}@YfE^WLnMBc;{S!Yw2+=%F@JlQ zBru~mRQnO3_4F&(wAj9O5j%pz7&*e4;R=J|tddN868*NWmBV<}UZs%UVR4lIJD5R$ z$2pu?{+GL?nCMk|CKcwXXuCf0t=+`+?OSE##ZA3hVcRb3Tw_`$oFoTo>Dl6;;Z95r z1O2Y;#ayC;K;HyhMdMC;u**?Wmr@Y}&Kdd-5i`z$0cp~{OTZyG8K^YhlWIfzeNh)F zL5!jv&pA5hogd?oeb=GAcKQ`#&`frEwYsriO)e^j$tn#!xG;m7b|9#iKt%Z*MWyj> z4b_SGvsD(q8~gRO!BTS3QoJTN7Ml8≪|f5Q}c`LX;$xQgf?i3^QH_NC9v9NW+BC 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%0Au(1Ocra7esypi+pj)Q~u2q_>>Y0)w%81L1J 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≺l%Ny7_UJb5% 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