@altimateai/altimate-code 0.5.19 → 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 CHANGED
@@ -5,6 +5,38 @@ 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
+
24
+ ## [0.5.20] - 2026-04-09
25
+
26
+ ### Added
27
+
28
+ - **Altimate model auto-selection** — when Altimate credentials are configured and no model is explicitly chosen, `altimate-backend/altimate-default` is selected automatically. Respects the `provider` filter in config if set. No manual `/model` selection needed for first-time Altimate users. (#665)
29
+
30
+ ### Fixed
31
+
32
+ - **Connection string passwords with special characters** — passwords containing `@`, `#`, `:`, `/`, or other URI-reserved characters are now automatically percent-encoded in `connection_string` configs. Previously these caused cryptic authentication failures because the URI parser split on the wrong delimiter. Already-encoded passwords (`%XX`) are left untouched. Affects all URI-based drivers (PostgreSQL, MongoDB, ClickHouse). (#597, closes #589)
33
+ - **`trace list` pagination** — `trace list` now supports `--offset` for navigating large trace histories, displays "Showing X-Y of N" with a next-page hint, and caps the TUI trace dialog at 500 items (up from 50) with an overflow message pointing to the CLI for the full set. (#596, closes #418)
34
+ - **ClickHouse edge-case hardening** — added tests for `LowCardinality(Nullable(...))` nullability detection, `Map`/`Tuple` wrapper handling, undefined type fallback, and SQL comment/string-escape edge cases in the LIMIT injection guard. (#599, closes #592)
35
+
36
+ ### Testing
37
+
38
+ - 31 new adversarial tests covering connection string sanitization (injection, encoding edge cases, ReDoS, Unicode, null bytes), pagination boundary math (Infinity, NaN, fractional, negative inputs), and `Provider.parseModel` edge cases.
39
+
8
40
  ## [0.5.19] - 2026-04-04
9
41
 
10
42
  ### Added
package/package.json CHANGED
@@ -7,24 +7,24 @@
7
7
  "scripts": {
8
8
  "postinstall": "bun ./postinstall.mjs || node ./postinstall.mjs"
9
9
  },
10
- "version": "0.5.19",
10
+ "version": "0.5.21",
11
11
  "license": "MIT",
12
12
  "dependencies": {
13
13
  "@altimateai/altimate-core": "0.2.6"
14
14
  },
15
15
  "optionalDependencies": {
16
- "@altimateai/altimate-code-linux-arm64": "0.5.19",
17
- "@altimateai/altimate-code-windows-x64": "0.5.19",
18
- "@altimateai/altimate-code-windows-x64-baseline": "0.5.19",
19
- "@altimateai/altimate-code-darwin-x64": "0.5.19",
20
- "@altimateai/altimate-code-windows-arm64": "0.5.19",
21
- "@altimateai/altimate-code-linux-x64-baseline": "0.5.19",
22
- "@altimateai/altimate-code-linux-arm64-musl": "0.5.19",
23
- "@altimateai/altimate-code-linux-x64-musl": "0.5.19",
24
- "@altimateai/altimate-code-linux-x64": "0.5.19",
25
- "@altimateai/altimate-code-darwin-x64-baseline": "0.5.19",
26
- "@altimateai/altimate-code-linux-x64-baseline-musl": "0.5.19",
27
- "@altimateai/altimate-code-darwin-arm64": "0.5.19"
16
+ "@altimateai/altimate-code-linux-x64-musl": "0.5.21",
17
+ "@altimateai/altimate-code-linux-x64-baseline": "0.5.21",
18
+ "@altimateai/altimate-code-windows-arm64": "0.5.21",
19
+ "@altimateai/altimate-code-darwin-arm64": "0.5.21",
20
+ "@altimateai/altimate-code-windows-x64-baseline": "0.5.21",
21
+ "@altimateai/altimate-code-linux-arm64": "0.5.21",
22
+ "@altimateai/altimate-code-linux-x64-baseline-musl": "0.5.21",
23
+ "@altimateai/altimate-code-darwin-x64-baseline": "0.5.21",
24
+ "@altimateai/altimate-code-windows-x64": "0.5.21",
25
+ "@altimateai/altimate-code-darwin-x64": "0.5.21",
26
+ "@altimateai/altimate-code-linux-x64": "0.5.21",
27
+ "@altimateai/altimate-code-linux-arm64-musl": "0.5.21"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "pg": ">=8",
@@ -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