altimate-code 0.5.20 → 0.5.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +13 -13
- package/skills/dbt-test/SKILL.md +2 -0
- package/skills/dbt-unit-tests/SKILL.md +209 -0
- package/skills/dbt-unit-tests/references/altimate-dbt-commands.md +66 -0
- package/skills/dbt-unit-tests/references/edge-case-patterns.md +189 -0
- package/skills/dbt-unit-tests/references/incremental-testing.md +109 -0
- package/skills/dbt-unit-tests/references/unit-test-yaml-spec.md +174 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.21] - 2026-04-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Automated dbt unit test generation** — generate dbt unit tests (v1.8+) from your terminal with `/dbt-unit-tests` or the `dbt_unit_test_gen` tool. Detects testable SQL constructs (CASE/WHEN, JOINs, NULLs, window functions, division, incremental models) and assembles complete YAML with type-correct mock data across 7 dialects. Includes `input: this` mocks for incremental models, `format: sql` for ephemeral deps, and handles seeds/snapshots as first-class `ref()` deps. Five-phase skill workflow: Analyze → Generate → Refine → Validate → Write. Requires dbt-core 1.8+. (#673)
|
|
13
|
+
- **Manifest parse cache** — `loadRawManifest()` caches by path+mtime so large manifests (100MB+) are parsed once per session, not once per tool call.
|
|
14
|
+
- **Model/source descriptions in manifest** — `DbtModelInfo` and `DbtSourceInfo` now surface descriptions from `schema.yml`, giving downstream tools richer semantic context.
|
|
15
|
+
- **`adapter_type` on `DbtManifestResult`** — exposes the dbt adapter type (snowflake, bigquery, etc.) from manifest metadata for dialect auto-detection.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **MCP env-var `$${VAR}` escape and chain-injection vulnerability** — the two-layer env-var resolution design allowed `$${VAR}` escapes to be re-resolved (breaking literal `${VAR}` passthrough) and enabled variable-chain injection where `EVIL_VAR="${SECRET}"` could exfiltrate secrets the config never referenced. Collapsed to a single resolution pass scoped to `env` and `headers` fields only. (#697, relates to #656)
|
|
20
|
+
- **MCP server environment variables passed as literals** — `${VAR}`, `${VAR:-default}`, and `{env:VAR}` patterns in MCP server `env` blocks were passed as literal strings to child processes, causing auth failures for tools like `gitlab-mcp-server`. (#666, closes #656)
|
|
21
|
+
- **`sql_explain` and `altimate_core_validate` input hardening** — reject empty/placeholder SQL and warehouse names before hitting the warehouse. `sql_explain` now generates dialect-aware EXPLAIN statements for 12+ warehouse types. Driver errors are translated into actionable guidance (e.g., "No warehouses configured — run `warehouse_add`"). `altimate_core_validate` now runs even without a schema (previously hard-failed), with a `(no schema)` indicator and clear instructions for providing schema context. (#693, closes #691)
|
|
22
|
+
- **`sql_explain` alternatives for unsupported warehouses** — BigQuery, Oracle, and SQL Server now return specific guidance (dry-run API, `DBMS_XPLAN`, `SET SHOWPLAN_TEXT ON`) instead of a generic "not supported" message.
|
|
23
|
+
|
|
8
24
|
## [0.5.20] - 2026-04-09
|
|
9
25
|
|
|
10
26
|
### Added
|
package/package.json
CHANGED
|
@@ -14,24 +14,24 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"postinstall": "bun ./postinstall.mjs || node ./postinstall.mjs"
|
|
16
16
|
},
|
|
17
|
-
"version": "0.5.
|
|
17
|
+
"version": "0.5.21",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@altimateai/altimate-core": "0.2.6"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@altimateai/altimate-code-linux-
|
|
24
|
-
"@altimateai/altimate-code-
|
|
25
|
-
"@altimateai/altimate-code-windows-
|
|
26
|
-
"@altimateai/altimate-code-darwin-
|
|
27
|
-
"@altimateai/altimate-code-windows-
|
|
28
|
-
"@altimateai/altimate-code-linux-
|
|
29
|
-
"@altimateai/altimate-code-linux-
|
|
30
|
-
"@altimateai/altimate-code-
|
|
31
|
-
"@altimateai/altimate-code-
|
|
32
|
-
"@altimateai/altimate-code-darwin-x64
|
|
33
|
-
"@altimateai/altimate-code-linux-x64
|
|
34
|
-
"@altimateai/altimate-code-
|
|
23
|
+
"@altimateai/altimate-code-linux-x64-musl": "0.5.21",
|
|
24
|
+
"@altimateai/altimate-code-linux-x64-baseline": "0.5.21",
|
|
25
|
+
"@altimateai/altimate-code-windows-arm64": "0.5.21",
|
|
26
|
+
"@altimateai/altimate-code-darwin-arm64": "0.5.21",
|
|
27
|
+
"@altimateai/altimate-code-windows-x64-baseline": "0.5.21",
|
|
28
|
+
"@altimateai/altimate-code-linux-arm64": "0.5.21",
|
|
29
|
+
"@altimateai/altimate-code-linux-x64-baseline-musl": "0.5.21",
|
|
30
|
+
"@altimateai/altimate-code-darwin-x64-baseline": "0.5.21",
|
|
31
|
+
"@altimateai/altimate-code-windows-x64": "0.5.21",
|
|
32
|
+
"@altimateai/altimate-code-darwin-x64": "0.5.21",
|
|
33
|
+
"@altimateai/altimate-code-linux-x64": "0.5.21",
|
|
34
|
+
"@altimateai/altimate-code-linux-arm64-musl": "0.5.21"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"pg": ">=8",
|
package/skills/dbt-test/SKILL.md
CHANGED
|
@@ -81,6 +81,8 @@ altimate-dbt build --model <name> # build + test together
|
|
|
81
81
|
|
|
82
82
|
## Unit Test Workflow
|
|
83
83
|
|
|
84
|
+
**For automated unit test generation, use the `dbt-unit-tests` skill instead.** It analyzes model SQL, generates type-correct mock data, and assembles complete YAML automatically.
|
|
85
|
+
|
|
84
86
|
See [references/unit-test-guide.md](references/unit-test-guide.md) for the full unit test framework.
|
|
85
87
|
|
|
86
88
|
### Quick Pattern
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dbt-unit-tests
|
|
3
|
+
description: Generate dbt unit tests automatically for any model. Analyzes SQL logic (CASE/WHEN, JOINs, window functions, NULLs), creates type-correct mock inputs from manifest schema, and assembles complete YAML. Use when a user says "generate tests", "add unit tests", "test this model", or "test coverage" for dbt models.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dbt Unit Test Generation
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
**Agent:** builder or migrator (requires file write access)
|
|
10
|
+
**Tools used:** dbt_unit_test_gen, dbt_manifest, dbt_lineage, altimate_core_validate, altimate_core_testgen, bash (runs `altimate-dbt` commands), read, glob, write, edit
|
|
11
|
+
|
|
12
|
+
## When to Use This Skill
|
|
13
|
+
|
|
14
|
+
**Use when the user wants to:**
|
|
15
|
+
- Generate unit tests for a dbt model
|
|
16
|
+
- Add test coverage to an existing model
|
|
17
|
+
- Create mock data for testing
|
|
18
|
+
- Test-driven development (TDD) for dbt
|
|
19
|
+
- Verify CASE/WHEN logic, NULL handling, JOIN behavior, or aggregation correctness
|
|
20
|
+
- Test incremental model logic
|
|
21
|
+
|
|
22
|
+
**Do NOT use for:**
|
|
23
|
+
- Adding schema tests (not_null, unique, accepted_values) -> use `dbt-test`
|
|
24
|
+
- Creating or modifying model SQL -> use `dbt-develop`
|
|
25
|
+
- Writing descriptions -> use `dbt-docs`
|
|
26
|
+
- Debugging build failures -> use `dbt-troubleshoot`
|
|
27
|
+
|
|
28
|
+
## The Iron Rules
|
|
29
|
+
|
|
30
|
+
1. **Never guess expected outputs.** Compute them by running SQL against mock data when possible. If you cannot run SQL, clearly mark expected outputs as placeholders that need verification.
|
|
31
|
+
2. **Never skip upstream dependencies.** Every ref() and source() the model touches MUST have a mock input. Miss one and the test won't compile.
|
|
32
|
+
3. **Use sql format for ephemeral models.** Dict format fails silently for ephemeral upstreams.
|
|
33
|
+
4. **Never weaken a test to make it pass.** If the test fails, the model logic may be wrong. Investigate before changing expected values.
|
|
34
|
+
5. **Compile before committing.** Always run `altimate-dbt test --model <name>` to verify tests compile and execute.
|
|
35
|
+
|
|
36
|
+
## Core Workflow: Analyze -> Generate -> Refine -> Validate -> Write
|
|
37
|
+
|
|
38
|
+
### Phase 1: Analyze the Model
|
|
39
|
+
|
|
40
|
+
Before generating any tests, deeply understand the model:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 1. Ensure manifest is compiled
|
|
44
|
+
altimate-dbt compile --model <name>
|
|
45
|
+
|
|
46
|
+
# 2. Read the model SQL
|
|
47
|
+
read <model_sql_file>
|
|
48
|
+
|
|
49
|
+
# 3. Parse the manifest for dependencies
|
|
50
|
+
dbt_unit_test_gen(manifest_path: "target/manifest.json", model: "<name>")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**What to look for:**
|
|
54
|
+
- Which upstream refs/sources does this model depend on?
|
|
55
|
+
- What SQL constructs need testing? (CASE/WHEN, JOINs, window functions, aggregations)
|
|
56
|
+
- What edge cases exist? (NULLs, empty strings, zero values, boundary dates)
|
|
57
|
+
- Is this an incremental model? (needs `is_incremental` override tests)
|
|
58
|
+
- Are any upstream models ephemeral? (need sql format)
|
|
59
|
+
|
|
60
|
+
### Phase 2: Generate Tests
|
|
61
|
+
|
|
62
|
+
The `dbt_unit_test_gen` tool does the heavy lifting:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
dbt_unit_test_gen(
|
|
66
|
+
manifest_path: "target/manifest.json",
|
|
67
|
+
model: "fct_orders",
|
|
68
|
+
max_scenarios: 5
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This returns:
|
|
73
|
+
- Complete YAML with mock inputs and expected outputs
|
|
74
|
+
- Semantic context: model/column descriptions, column lineage, compiled SQL
|
|
75
|
+
- List of anti-patterns that informed edge case generation
|
|
76
|
+
- Warnings about ephemeral deps, missing columns, etc.
|
|
77
|
+
|
|
78
|
+
**If the tool reports missing columns** (placeholder rows in the YAML), discover them:
|
|
79
|
+
```bash
|
|
80
|
+
altimate-dbt columns --model <upstream_model_name>
|
|
81
|
+
altimate-dbt columns-source --source <source_name> --table <table_name>
|
|
82
|
+
```
|
|
83
|
+
Then update the generated YAML with real column names.
|
|
84
|
+
|
|
85
|
+
### Phase 3: Refine Expected Outputs
|
|
86
|
+
|
|
87
|
+
**This is the critical step that differentiates good tests from bad ones.**
|
|
88
|
+
|
|
89
|
+
The tool generates placeholder expected outputs based on column types. You MUST refine them:
|
|
90
|
+
|
|
91
|
+
**Option A: Compute by running SQL (preferred)**
|
|
92
|
+
```bash
|
|
93
|
+
# Run the model against mock data to get actual output
|
|
94
|
+
altimate-dbt test --model <name>
|
|
95
|
+
# If the test fails, the error shows actual vs expected — use actual as expected
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Option B: Manual computation**
|
|
99
|
+
Read the model SQL carefully and mentally execute it against the mock inputs.
|
|
100
|
+
For each test case:
|
|
101
|
+
1. Look at the mock input rows
|
|
102
|
+
2. Trace through the SQL logic (CASE/WHEN branches, JOINs, aggregations)
|
|
103
|
+
3. Write the correct expected output
|
|
104
|
+
|
|
105
|
+
**Option C: Use the warehouse (most accurate)**
|
|
106
|
+
```bash
|
|
107
|
+
# Build a CTE query with mock data and run the model SQL against it
|
|
108
|
+
altimate-dbt execute --query "WITH mock_stg_orders AS (SELECT 1 AS order_id, 100.00 AS amount) SELECT * FROM (<model_sql>) sub"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Phase 4: Validate
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# 1. Run the unit tests
|
|
115
|
+
altimate-dbt test --model <name>
|
|
116
|
+
|
|
117
|
+
# 2. If tests fail, read the error carefully
|
|
118
|
+
# - Compilation error? Missing ref, wrong column name, type mismatch
|
|
119
|
+
# - Assertion error? Expected output doesn't match actual
|
|
120
|
+
|
|
121
|
+
# 3. Fix and retry (max 3 iterations)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Phase 5: Write to File
|
|
125
|
+
|
|
126
|
+
Place unit tests in one of these locations (match project convention):
|
|
127
|
+
- `models/<layer>/_unit_tests.yml` (dedicated file)
|
|
128
|
+
- `models/<layer>/schema.yml` (append to existing)
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Check existing convention
|
|
132
|
+
glob models/**/*unit_test*.yml models/**/*schema*.yml
|
|
133
|
+
|
|
134
|
+
# Write or append
|
|
135
|
+
edit <yaml_file> # if file exists
|
|
136
|
+
write <yaml_file> # if creating new
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Test Case Categories
|
|
140
|
+
|
|
141
|
+
### Happy Path (always generate)
|
|
142
|
+
Standard inputs that exercise the main logic path. 2 rows minimum.
|
|
143
|
+
|
|
144
|
+
### NULL Handling
|
|
145
|
+
Set nullable columns to NULL in the last row. Verify COALESCE/NVL/IFNULL behavior.
|
|
146
|
+
|
|
147
|
+
### Boundary Values
|
|
148
|
+
Zero amounts, empty strings, epoch dates, MAX values. Tests robustness.
|
|
149
|
+
|
|
150
|
+
### Edge Cases
|
|
151
|
+
- Division by zero (if model divides)
|
|
152
|
+
- Non-matching JOINs (LEFT JOIN with no match)
|
|
153
|
+
- Single-row aggregation
|
|
154
|
+
- Duplicate key handling
|
|
155
|
+
|
|
156
|
+
### Incremental
|
|
157
|
+
For incremental models only. Use `overrides.macros.is_incremental: true` to test the incremental path.
|
|
158
|
+
|
|
159
|
+
## Common Mistakes
|
|
160
|
+
|
|
161
|
+
| Mistake | Fix |
|
|
162
|
+
|---------|-----|
|
|
163
|
+
| Missing a ref() in given | Parse manifest for ALL depends_on nodes |
|
|
164
|
+
| Wrong column names in mock data | Use manifest columns, not guesses |
|
|
165
|
+
| Wrong data types | Use schema catalog types |
|
|
166
|
+
| Expected output is just mock input | Actually compute the transformation |
|
|
167
|
+
| Dict format for ephemeral model | Use `format: sql` with raw SQL |
|
|
168
|
+
| Not testing NULL path in COALESCE | Add null_handling test case |
|
|
169
|
+
| Hardcoded dates with current_timestamp | Use overrides.macros to mock timestamps |
|
|
170
|
+
| Testing trivial pass-through | Skip models with no logic |
|
|
171
|
+
|
|
172
|
+
## YAML Format Reference
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
unit_tests:
|
|
176
|
+
- name: test_<model>_<scenario>
|
|
177
|
+
description: "What this test verifies"
|
|
178
|
+
model: <model_name>
|
|
179
|
+
overrides: # optional
|
|
180
|
+
macros:
|
|
181
|
+
is_incremental: true # for incremental models
|
|
182
|
+
vars:
|
|
183
|
+
run_date: "2024-01-15" # for date-dependent logic
|
|
184
|
+
given:
|
|
185
|
+
- input: ref('upstream_model')
|
|
186
|
+
rows:
|
|
187
|
+
- { col1: value1, col2: value2 }
|
|
188
|
+
- input: source('source_name', 'table_name')
|
|
189
|
+
rows:
|
|
190
|
+
- { col1: value1 }
|
|
191
|
+
- input: ref('ephemeral_model')
|
|
192
|
+
format: sql
|
|
193
|
+
rows: |
|
|
194
|
+
SELECT 1 AS id, 'test' AS name
|
|
195
|
+
UNION ALL
|
|
196
|
+
SELECT 2 AS id, 'other' AS name
|
|
197
|
+
expect:
|
|
198
|
+
rows:
|
|
199
|
+
- { output_col1: expected1, output_col2: expected2 }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Reference Guides
|
|
203
|
+
|
|
204
|
+
| Guide | Use When |
|
|
205
|
+
|-------|----------|
|
|
206
|
+
| [references/unit-test-yaml-spec.md](references/unit-test-yaml-spec.md) | Full YAML specification and format details |
|
|
207
|
+
| [references/edge-case-patterns.md](references/edge-case-patterns.md) | Catalog of edge cases by SQL construct |
|
|
208
|
+
| [references/incremental-testing.md](references/incremental-testing.md) | Testing incremental models |
|
|
209
|
+
| [references/altimate-dbt-commands.md](references/altimate-dbt-commands.md) | Full CLI reference |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# altimate-dbt Command Reference
|
|
2
|
+
|
|
3
|
+
All dbt operations use the `altimate-dbt` CLI. Output is JSON to stdout; logs go to stderr.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
altimate-dbt <command> [args...]
|
|
7
|
+
altimate-dbt <command> [args...] --format text # Human-readable output
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## First-Time Setup
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
altimate-dbt init # Auto-detect project root
|
|
14
|
+
altimate-dbt init --project-root /path # Explicit root
|
|
15
|
+
altimate-dbt init --python-path /path # Override Python
|
|
16
|
+
altimate-dbt doctor # Verify setup
|
|
17
|
+
altimate-dbt info # Project name, adapter, root
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Build & Run
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
altimate-dbt build # full project build (compile + run + test)
|
|
24
|
+
altimate-dbt build --model <name> [--downstream] # build a single model
|
|
25
|
+
altimate-dbt run --model <name> [--downstream] # materialize only
|
|
26
|
+
altimate-dbt test --model <name> # run tests only
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Compile
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
altimate-dbt compile --model <name>
|
|
33
|
+
altimate-dbt compile-query --query "SELECT * FROM {{ ref('stg_orders') }}" [--model <context>]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Execute SQL
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
altimate-dbt execute --query "SELECT count(*) FROM {{ ref('orders') }}" --limit 100
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Schema & DAG
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
altimate-dbt columns --model <name> # column names and types
|
|
46
|
+
altimate-dbt columns-source --source <src> --table <tbl> # source table columns
|
|
47
|
+
altimate-dbt column-values --model <name> --column <col> # sample values
|
|
48
|
+
altimate-dbt children --model <name> # downstream models
|
|
49
|
+
altimate-dbt parents --model <name> # upstream models
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Packages
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
altimate-dbt deps # install packages.yml
|
|
56
|
+
altimate-dbt add-packages --packages dbt-utils,dbt-expectations
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Error Handling
|
|
60
|
+
|
|
61
|
+
All errors return JSON with `error` and `fix` fields:
|
|
62
|
+
```json
|
|
63
|
+
{ "error": "dbt-core is not installed", "fix": "Install it: python3 -m pip install dbt-core" }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Run `altimate-dbt doctor` as the first diagnostic step for any failure.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Edge Case Patterns by SQL Construct
|
|
2
|
+
|
|
3
|
+
## CASE/WHEN
|
|
4
|
+
|
|
5
|
+
**What to test:** Every branch, including ELSE/default.
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
# Test the TRUE branch
|
|
9
|
+
- { status: "completed", amount: 100 }
|
|
10
|
+
# Expected: { category: "done" }
|
|
11
|
+
|
|
12
|
+
# Test the FALSE/ELSE branch
|
|
13
|
+
- { status: "unknown", amount: 100 }
|
|
14
|
+
# Expected: { category: "other" }
|
|
15
|
+
|
|
16
|
+
# Test NULL input
|
|
17
|
+
- { status: null, amount: 100 }
|
|
18
|
+
# Expected: depends on whether NULL matches any WHEN
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Common bugs:**
|
|
22
|
+
- NULL doesn't match `WHEN status = 'active'` — it falls to ELSE
|
|
23
|
+
- Multiple WHEN clauses: first match wins, test ordering
|
|
24
|
+
|
|
25
|
+
## COALESCE / NVL / IFNULL
|
|
26
|
+
|
|
27
|
+
**What to test:** NULL in each position.
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
# COALESCE(a, b, c) — test a=NULL
|
|
31
|
+
- { a: null, b: "fallback", c: "default" }
|
|
32
|
+
# Expected: { result: "fallback" }
|
|
33
|
+
|
|
34
|
+
# COALESCE(a, b, c) — test a=NULL, b=NULL
|
|
35
|
+
- { a: null, b: null, c: "default" }
|
|
36
|
+
# Expected: { result: "default" }
|
|
37
|
+
|
|
38
|
+
# All non-null
|
|
39
|
+
- { a: "primary", b: "fallback", c: "default" }
|
|
40
|
+
# Expected: { result: "primary" }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## JOINs
|
|
44
|
+
|
|
45
|
+
**What to test:** Matching rows, non-matching rows, NULL join keys.
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
# LEFT JOIN — matching row
|
|
49
|
+
orders: [{ order_id: 1, customer_id: 1 }]
|
|
50
|
+
customers: [{ customer_id: 1, name: "Alice" }]
|
|
51
|
+
# Expected: { order_id: 1, name: "Alice" }
|
|
52
|
+
|
|
53
|
+
# LEFT JOIN — no match (customer missing)
|
|
54
|
+
orders: [{ order_id: 2, customer_id: 99 }]
|
|
55
|
+
customers: [{ customer_id: 1, name: "Alice" }]
|
|
56
|
+
# Expected: { order_id: 2, name: null }
|
|
57
|
+
|
|
58
|
+
# JOIN with NULL key
|
|
59
|
+
orders: [{ order_id: 3, customer_id: null }]
|
|
60
|
+
customers: [{ customer_id: 1, name: "Alice" }]
|
|
61
|
+
# Expected: depends on join type
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Common bugs:**
|
|
65
|
+
- INNER JOIN drops rows when key is NULL or missing
|
|
66
|
+
- Fan-out: duplicate keys in right table multiply left rows
|
|
67
|
+
|
|
68
|
+
## Window Functions
|
|
69
|
+
|
|
70
|
+
**What to test:** Ordering, partitioning, boundary rows.
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
# ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date)
|
|
74
|
+
- { customer_id: 1, order_date: "2024-01-01", amount: 50 }
|
|
75
|
+
- { customer_id: 1, order_date: "2024-01-15", amount: 75 }
|
|
76
|
+
- { customer_id: 2, order_date: "2024-01-10", amount: 30 }
|
|
77
|
+
# Expected:
|
|
78
|
+
# { customer_id: 1, order_date: "2024-01-01", row_num: 1 }
|
|
79
|
+
# { customer_id: 1, order_date: "2024-01-15", row_num: 2 }
|
|
80
|
+
# { customer_id: 2, order_date: "2024-01-10", row_num: 1 }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**What to test for LAG/LEAD:**
|
|
84
|
+
- First/last row in partition (LAG returns NULL for first row)
|
|
85
|
+
- Single-row partition
|
|
86
|
+
|
|
87
|
+
## Aggregations (GROUP BY)
|
|
88
|
+
|
|
89
|
+
**What to test:** Multiple groups, single group, empty group.
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
# SUM(amount) GROUP BY customer_id
|
|
93
|
+
- { customer_id: 1, amount: 50 }
|
|
94
|
+
- { customer_id: 1, amount: 25 }
|
|
95
|
+
- { customer_id: 2, amount: 100 }
|
|
96
|
+
# Expected:
|
|
97
|
+
# { customer_id: 1, total_amount: 75 }
|
|
98
|
+
# { customer_id: 2, total_amount: 100 }
|
|
99
|
+
|
|
100
|
+
# Single row group
|
|
101
|
+
- { customer_id: 3, amount: 10 }
|
|
102
|
+
# Expected: { customer_id: 3, total_amount: 10 }
|
|
103
|
+
|
|
104
|
+
# NULL in aggregated column
|
|
105
|
+
- { customer_id: 4, amount: null }
|
|
106
|
+
# Expected: { customer_id: 4, total_amount: null } # SUM of NULLs = NULL
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Division
|
|
110
|
+
|
|
111
|
+
**What to test:** Normal, divide by zero, NULL.
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
# amount / quantity
|
|
115
|
+
- { amount: 100, quantity: 4 }
|
|
116
|
+
# Expected: { unit_price: 25 }
|
|
117
|
+
|
|
118
|
+
# Divide by zero
|
|
119
|
+
- { amount: 100, quantity: 0 }
|
|
120
|
+
# Expected: depends — NULL, error, or COALESCE fallback?
|
|
121
|
+
|
|
122
|
+
# NULL divisor
|
|
123
|
+
- { amount: 100, quantity: null }
|
|
124
|
+
# Expected: { unit_price: null }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Date/Timestamp Logic
|
|
128
|
+
|
|
129
|
+
**What to test:** Boundaries, NULL dates, timezone edge cases.
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
# DATEDIFF or date filtering
|
|
133
|
+
- { event_date: "2024-01-01" } # start of year
|
|
134
|
+
- { event_date: "2024-12-31" } # end of year
|
|
135
|
+
- { event_date: "2024-02-29" } # leap year
|
|
136
|
+
- { event_date: null } # NULL date
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Type Coercion
|
|
140
|
+
|
|
141
|
+
**What to test:** Implicit casts that may fail.
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
# String that looks like a number
|
|
145
|
+
- { amount_str: "100.50" } # CAST to DECIMAL
|
|
146
|
+
- { amount_str: "not_a_number" } # should this fail?
|
|
147
|
+
- { amount_str: "" } # empty string cast
|
|
148
|
+
- { amount_str: null } # NULL cast
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Incremental Models
|
|
152
|
+
|
|
153
|
+
**What to test:** Full refresh vs incremental path.
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
# Test 1: Full refresh (is_incremental = false, default)
|
|
157
|
+
# All rows processed
|
|
158
|
+
|
|
159
|
+
# Test 2: Incremental (is_incremental = true)
|
|
160
|
+
unit_tests:
|
|
161
|
+
- name: test_incremental_new_rows_only
|
|
162
|
+
model: fct_orders
|
|
163
|
+
overrides:
|
|
164
|
+
macros:
|
|
165
|
+
is_incremental: true
|
|
166
|
+
given:
|
|
167
|
+
- input: this # existing table state
|
|
168
|
+
rows:
|
|
169
|
+
- { order_id: 1, updated_at: "2024-01-14" }
|
|
170
|
+
- input: ref('stg_orders')
|
|
171
|
+
rows:
|
|
172
|
+
- { order_id: 1, updated_at: "2024-01-14" } # old, should be skipped
|
|
173
|
+
- { order_id: 2, updated_at: "2024-01-15" } # new, should be processed
|
|
174
|
+
expect:
|
|
175
|
+
rows:
|
|
176
|
+
- { order_id: 2, updated_at: "2024-01-15" }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Empty Inputs
|
|
180
|
+
|
|
181
|
+
**What to test:** Model behavior when upstream has zero rows.
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
given:
|
|
185
|
+
- input: ref('stg_orders')
|
|
186
|
+
rows: []
|
|
187
|
+
expect:
|
|
188
|
+
rows: [] # or specific default behavior
|
|
189
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Testing Incremental dbt Models
|
|
2
|
+
|
|
3
|
+
Incremental models have two code paths controlled by `{% if is_incremental() %}`. Both paths must be tested.
|
|
4
|
+
|
|
5
|
+
## The Two Paths
|
|
6
|
+
|
|
7
|
+
```sql
|
|
8
|
+
SELECT * FROM {{ ref('stg_orders') }}
|
|
9
|
+
|
|
10
|
+
{% if is_incremental() %}
|
|
11
|
+
-- Incremental path: only process new rows
|
|
12
|
+
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
|
|
13
|
+
{% endif %}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Test 1: Full Refresh
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
unit_tests:
|
|
20
|
+
- name: test_fct_orders_full_refresh
|
|
21
|
+
description: "Full refresh processes all rows"
|
|
22
|
+
model: fct_orders
|
|
23
|
+
# No overrides needed — is_incremental defaults to false
|
|
24
|
+
given:
|
|
25
|
+
- input: ref('stg_orders')
|
|
26
|
+
rows:
|
|
27
|
+
- { order_id: 1, amount: 100, updated_at: "2024-01-10" }
|
|
28
|
+
- { order_id: 2, amount: 200, updated_at: "2024-01-15" }
|
|
29
|
+
expect:
|
|
30
|
+
rows:
|
|
31
|
+
- { order_id: 1, amount: 100, updated_at: "2024-01-10" }
|
|
32
|
+
- { order_id: 2, amount: 200, updated_at: "2024-01-15" }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Test 2: Incremental — New Rows Only
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
unit_tests:
|
|
39
|
+
- name: test_fct_orders_incremental_new_only
|
|
40
|
+
description: "Incremental run only processes rows newer than existing max"
|
|
41
|
+
model: fct_orders
|
|
42
|
+
overrides:
|
|
43
|
+
macros:
|
|
44
|
+
is_incremental: true
|
|
45
|
+
given:
|
|
46
|
+
- input: this # mock the existing target table
|
|
47
|
+
rows:
|
|
48
|
+
- { order_id: 1, amount: 100, updated_at: "2024-01-10" }
|
|
49
|
+
- input: ref('stg_orders')
|
|
50
|
+
rows:
|
|
51
|
+
- { order_id: 1, amount: 100, updated_at: "2024-01-10" } # old
|
|
52
|
+
- { order_id: 2, amount: 200, updated_at: "2024-01-15" } # new
|
|
53
|
+
expect:
|
|
54
|
+
rows:
|
|
55
|
+
- { order_id: 2, amount: 200, updated_at: "2024-01-15" }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Test 3: Incremental — Updated Rows
|
|
59
|
+
|
|
60
|
+
If your model uses `unique_key` for merge/upsert:
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
unit_tests:
|
|
64
|
+
- name: test_fct_orders_incremental_update
|
|
65
|
+
description: "Updated rows are captured in incremental run"
|
|
66
|
+
model: fct_orders
|
|
67
|
+
overrides:
|
|
68
|
+
macros:
|
|
69
|
+
is_incremental: true
|
|
70
|
+
given:
|
|
71
|
+
- input: this
|
|
72
|
+
rows:
|
|
73
|
+
- { order_id: 1, amount: 100, updated_at: "2024-01-10" }
|
|
74
|
+
- input: ref('stg_orders')
|
|
75
|
+
rows:
|
|
76
|
+
- { order_id: 1, amount: 150, updated_at: "2024-01-15" } # updated
|
|
77
|
+
expect:
|
|
78
|
+
rows:
|
|
79
|
+
- { order_id: 1, amount: 150, updated_at: "2024-01-15" }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Test 4: Incremental — Empty Source
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
unit_tests:
|
|
86
|
+
- name: test_fct_orders_incremental_no_new_data
|
|
87
|
+
description: "No new rows when source has nothing newer"
|
|
88
|
+
model: fct_orders
|
|
89
|
+
overrides:
|
|
90
|
+
macros:
|
|
91
|
+
is_incremental: true
|
|
92
|
+
given:
|
|
93
|
+
- input: this
|
|
94
|
+
rows:
|
|
95
|
+
- { order_id: 1, amount: 100, updated_at: "2024-01-15" }
|
|
96
|
+
- input: ref('stg_orders')
|
|
97
|
+
rows:
|
|
98
|
+
- { order_id: 1, amount: 100, updated_at: "2024-01-10" } # older
|
|
99
|
+
expect:
|
|
100
|
+
rows: []
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Key Points
|
|
104
|
+
|
|
105
|
+
1. **Always mock `this`** when testing incremental path — it represents the existing target table
|
|
106
|
+
2. **Set `is_incremental: true`** in overrides.macros to activate the incremental code path
|
|
107
|
+
3. **Test both paths** — full refresh AND incremental
|
|
108
|
+
4. **Include overlap rows** — rows that exist in both `this` and source to verify filtering
|
|
109
|
+
5. **Test the merge key** — if `unique_key` is set, verify upsert behavior
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# dbt Unit Test YAML Specification
|
|
2
|
+
|
|
3
|
+
Available in dbt-core 1.8+ (released mid-2024).
|
|
4
|
+
|
|
5
|
+
## Top-Level Structure
|
|
6
|
+
|
|
7
|
+
Unit tests are defined under the `unit_tests:` key in any YAML file within your dbt project (typically `schema.yml` or `_unit_tests.yml`).
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
unit_tests:
|
|
11
|
+
- name: <test_name> # required, snake_case
|
|
12
|
+
description: <string> # optional but recommended
|
|
13
|
+
model: <model_name> # required, the model being tested
|
|
14
|
+
given: <list of inputs> # required, mock input data
|
|
15
|
+
expect: <expected output> # required, expected output rows
|
|
16
|
+
overrides: <overrides> # optional, macro/var overrides
|
|
17
|
+
config: <config> # optional, test configuration
|
|
18
|
+
tags: <list> # optional, for filtering
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Input Formats
|
|
22
|
+
|
|
23
|
+
### Dict Format (default, preferred)
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
given:
|
|
27
|
+
- input: ref('stg_orders')
|
|
28
|
+
rows:
|
|
29
|
+
- { order_id: 1, amount: 100.00, status: "completed" }
|
|
30
|
+
- { order_id: 2, amount: null, status: "pending" }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Rules:**
|
|
34
|
+
- Only include columns that the model actually uses
|
|
35
|
+
- Column names must match the upstream model exactly
|
|
36
|
+
- Use `null` for NULL values (not empty string)
|
|
37
|
+
- Dates as strings: `"2024-01-15"`
|
|
38
|
+
- Timestamps as strings: `"2024-01-15 10:30:00"`
|
|
39
|
+
- Booleans: `true` / `false`
|
|
40
|
+
- Numbers: no quotes (`100.00`, not `"100.00"`)
|
|
41
|
+
|
|
42
|
+
### SQL Format (required for ephemeral models)
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
given:
|
|
46
|
+
- input: ref('ephemeral_model')
|
|
47
|
+
format: sql
|
|
48
|
+
rows: |
|
|
49
|
+
SELECT 1 AS id, 'test' AS name
|
|
50
|
+
UNION ALL
|
|
51
|
+
SELECT 2 AS id, 'other' AS name
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**When to use SQL format:**
|
|
55
|
+
- Upstream model is materialized as `ephemeral`
|
|
56
|
+
- Complex data types that dict can't represent
|
|
57
|
+
- Need to use SQL functions in mock data
|
|
58
|
+
|
|
59
|
+
### Empty Input
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
given:
|
|
63
|
+
- input: ref('stg_orders')
|
|
64
|
+
rows: []
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Tests behavior with no input rows (empty table).
|
|
68
|
+
|
|
69
|
+
## Expected Output
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
expect:
|
|
73
|
+
rows:
|
|
74
|
+
- { order_id: 1, net_revenue: 85.00 }
|
|
75
|
+
- { order_id: 2, net_revenue: 50.00 }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Rules:**
|
|
79
|
+
- Only include columns you want to assert on (subset is OK)
|
|
80
|
+
- Row order matters — rows are compared positionally
|
|
81
|
+
- Use exact values for numeric assertions
|
|
82
|
+
- `null` to assert NULL output
|
|
83
|
+
|
|
84
|
+
## Overrides
|
|
85
|
+
|
|
86
|
+
### Macro Overrides
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
overrides:
|
|
90
|
+
macros:
|
|
91
|
+
is_incremental: true # boolean
|
|
92
|
+
current_timestamp: "2024-01-15 00:00:00" # string
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Common macros to override:
|
|
96
|
+
- `is_incremental` — test incremental vs full-refresh path
|
|
97
|
+
- `current_timestamp` / `current_date` — deterministic date testing
|
|
98
|
+
|
|
99
|
+
### Variable Overrides
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
overrides:
|
|
103
|
+
vars:
|
|
104
|
+
run_date: "2024-01-15"
|
|
105
|
+
lookback_days: 30
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Input Sources
|
|
109
|
+
|
|
110
|
+
### ref() — Model references
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
- input: ref('model_name')
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### source() — Source table references
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
- input: source('source_name', 'table_name')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### this — Self-reference for incremental models
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
- input: this
|
|
126
|
+
rows:
|
|
127
|
+
- { order_id: 1, updated_at: "2024-01-14" }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Used with `overrides.macros.is_incremental: true` to mock the existing table state.
|
|
131
|
+
|
|
132
|
+
## Configuration
|
|
133
|
+
|
|
134
|
+
Tags can be set at the top level (sibling of `config`) or nested under `config`:
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
unit_tests:
|
|
138
|
+
- name: test_example
|
|
139
|
+
model: fct_orders
|
|
140
|
+
tags: ["unit-test", "revenue"]
|
|
141
|
+
# ... rest of test
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Or via config:
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
unit_tests:
|
|
148
|
+
- name: test_example
|
|
149
|
+
model: fct_orders
|
|
150
|
+
config:
|
|
151
|
+
tags: ["unit-test", "revenue"]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Naming Conventions
|
|
155
|
+
|
|
156
|
+
- Test names: `test_<model>_<what_it_tests>`
|
|
157
|
+
- Examples:
|
|
158
|
+
- `test_fct_orders_happy_path`
|
|
159
|
+
- `test_fct_orders_null_discount`
|
|
160
|
+
- `test_fct_orders_zero_quantity`
|
|
161
|
+
- `test_fct_orders_incremental_new_rows`
|
|
162
|
+
|
|
163
|
+
## Running Unit Tests
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
dbt test --select test_type:unit # all unit tests
|
|
167
|
+
dbt test --select test_type:unit,model_name:fct_orders # unit tests for one model
|
|
168
|
+
dbt build --select +fct_orders # build + all tests
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Official Documentation
|
|
172
|
+
|
|
173
|
+
- https://docs.getdbt.com/docs/build/unit-tests
|
|
174
|
+
- https://docs.getdbt.com/reference/resource-properties/unit-tests
|