altimate-code 0.5.21 → 0.6.0
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 +41 -0
- package/package.json +14 -14
- package/skills/data-parity/SKILL.md +492 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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.6.0] - 2026-04-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Data parity (`data_diff` tool + skill)** — compare tables or SQL-query results row-by-row across Postgres, Snowflake, BigQuery, Databricks, ClickHouse, MySQL, Redshift, SQL Server, Microsoft Fabric, DuckDB, SQLite, and Oracle. Five algorithms: `auto` (JoinDiff same-dialect, HashDiff cross-dialect), `joindiff` (FULL OUTER JOIN), `hashdiff` (bisecting checksums — works at any scale without pulling data out), `profile` (column-level statistics, no row values leave the database), and `cascade` (profile first, then HashDiff on diverging columns). Partitioning supports date (`day`/`week`/`month`/`year`), numeric (`bucket_size`), and categorical (distinct values) modes so 100M+ row tables diff in independent batches. Auto-discovers comparable columns from `information_schema`, excludes audit/timestamp columns by name pattern AND by catalog default (`NOW()`, `CURRENT_TIMESTAMP`, `GETDATE()`, `SYSDATE`, `SYSTIMESTAMP`), and confirms exclusions with the user before diffing. (#493)
|
|
13
|
+
- **MSSQL and Microsoft Fabric support in data-parity** — dialect-aware date truncation (`DATETRUNC`), locale-safe date literals (`CONVERT(DATE, ..., 23)`), and seven Azure AD / Entra ID authentication flows (password, access-token, service-principal-secret, MSI-VM, MSI-app-service, default credential chain, token-credential) delegated to `tedious`. Upgrades `mssql` v11 → v12 with explicit `ConnectionPool` isolation and correct handling of unnamed-column result sets. (#705)
|
|
14
|
+
- **Databricks AI Gateway provider** — connect to Databricks serving endpoints (Foundation Model APIs) via PAT auth (`workspace-host::token`), with fallback to `DATABRICKS_HOST` / `DATABRICKS_TOKEN` environment variables. Registers 11 foundation models — Meta Llama 3.1 (405B / 70B / 8B), Claude Sonnet / Opus 4.6, GPT-5.4 / GPT-5 Mini, Gemini 3.1 Pro, DBRX Instruct, and Mixtral 8x7B. Host regex restricts credentials to `*.cloud.databricks.com`, `*.azuredatabricks.net`, and `*.gcp.databricks.com`. (#649, closes #602)
|
|
15
|
+
- **Amazon Bedrock custom-endpoints guide** — dedicated docs page covering bearer-token auth, AWS credential chain, `baseURL` configuration, cross-region model-ID prefixing, and troubleshooting. Provider key corrected from `bedrock` to `amazon-bedrock` across quickstart, providers, and models pages. (#706)
|
|
16
|
+
- **User-facing docs for the new features** — Databricks AI Gateway section in `configure/providers.md` and a full `data-engineering/guides/data-parity.md` covering supported warehouse pairs, algorithms, partition modes, Azure AD auth matrix for Fabric, and compliance guidance.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **`@altimateai/altimate-core` 0.2.6 → 0.3.0** — enables the cross-dialect data-parity engine, T-SQL dialect support for MSSQL/Fabric, and refined hashdiff bisection. Rebuilt native binaries published for all five supported platforms. (#717, closes #716)
|
|
21
|
+
- **Altimate connect dialog polish** — `/connect` now accepts `instance-name::api-key` directly (default URL `https://api.myaltimate.com`). The three-part `api-url::instance-name::api-key` form still works for custom and self-hosted instances. Provider display name "Altimate" → "Altimate AI"; default model display "Altimate AI" → "Altimate LLM Gateway". The internal provider ID (`altimate-backend`) and model ID (`altimate-default`) are preserved — existing `model.json` favorites, recents, and pinned `model:` entries in `opencode.json` continue to work without migration. (#724)
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **Text contrast on light terminal backgrounds** — dark foreground (`#1a1a1a` or `palette[0]` when available) replaces the near-invisible `palette[7]` on light-mode system themes, and inline code blocks now render on an opaque background instead of transparent. Eager `COLORFGBG` env-var detection narrowed to `bg === 7 || bg === 15` skips the 1-second OSC 11 query altogether on light terminals that don't support it (urxvt, gnome-terminal). (#712, closes #704)
|
|
26
|
+
- **Historical `tool_use` blocks after agent switches or MCP disconnects** — the LiteLLM-only `_noop` workaround is replaced by a general fix: tool names are extracted from both `tool-call` and `tool-result` blocks in message history (using `Object.hasOwn()` for prototype-pollution safety), validated against `/^[a-zA-Z0-9_-]{1,64}$/`, and registered as stubs that return "tool no longer available" if the model attempts to call them. Eliminates the Anthropic API 400 error "Requests with 'tool_use' and 'tool_result' blocks must include tool definition." (#703, closes [AI-678])
|
|
27
|
+
- **`/docs` command and config links point to `docs.altimate.sh`** — the TUI "Open docs" action, Cloudflare AI Gateway help text, Anthropic system prompt, and `Config.command` / `Config.agent` schema descriptions previously linked to the wrong domain. Also updates paths to the `/configure/` prefix that matches the actual mkdocs site. (#715, closes #714)
|
|
28
|
+
|
|
29
|
+
### Security
|
|
30
|
+
|
|
31
|
+
- **Databricks host validation hardened** — new `isValidDatabricksHost` helper rejects CRLF/whitespace (JS regex `$` matches before `\n` by default), and the env-fallback path now validates host before constructing the `baseURL`.
|
|
32
|
+
- **Tool-name validation** — stub registration ignores names with shell metacharacters, ANSI escapes, control characters, or lengths > 64, guarding against tampered session-file replays.
|
|
33
|
+
- **Restricted `az` inherited env** — `az account get-access-token` is invoked with a whitelisted environment (`PATH`, `HOME`, `AZURE_*`, locale) so unrelated secrets (`DATABRICKS_TOKEN`, cloud provider keys) are not inherited by `az` or any `az` extension.
|
|
34
|
+
|
|
35
|
+
### Testing
|
|
36
|
+
|
|
37
|
+
- 38 new adversarial tests covering Databricks host validation (CRLF, anchoring, attacker suffixes), PAT parsing edge cases, body transform, tool-name tainted-input guards, and `data_diff` tool-description release contract.
|
|
38
|
+
- 139-test consolidation across dbt helpers, file status, project-scan, session/llm, and MCP discovery — symlink cache round-trips, seed/test node exclusion, JSON array edge cases, sessionId sanitization, pagination boundary math, and connection-string masking. Two broken `${VAR}`-in-MCP-`command` tests removed (resolution was always restricted to `env` and `headers`). (#709)
|
|
39
|
+
|
|
40
|
+
### Compliance note
|
|
41
|
+
|
|
42
|
+
`data_diff` includes up to 5 sample diff rows in its tool output, which becomes part of the LLM conversation. For PII / PHI / PCI data, use `algorithm: "profile"` — column statistics compare without sending row values. The `data-parity` skill asks for explicit confirmation before running row-level diffs against tables whose names match common regulated patterns (`customers`, `patients`, `orders`, `payments`, `accounts`, `users`). A hard env-var opt-out for sample values is tracked in [#729](https://github.com/AltimateAI/altimate-code/issues/729).
|
|
43
|
+
|
|
44
|
+
### Breaking
|
|
45
|
+
|
|
46
|
+
- **`mssql` upgraded to v12**. Users with `mssql@^11` pinned at the application level will see "mssql.ConnectionPool is not available" on first SQL Server connection. Pin to `^12` or let altimate resolve.
|
|
47
|
+
- **SQL Server result sets now expose `_`-prefixed columns**. The internal `startsWith("_")` column filter (introduced for partition-discovery noise suppression) was removed because it also stripped legitimate aliases like `_p` used by the partition engine. Queries that relied on this implicit filtering will see the extra columns in results.
|
|
48
|
+
|
|
8
49
|
## [0.5.21] - 2026-04-13
|
|
9
50
|
|
|
10
51
|
### 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.
|
|
17
|
+
"version": "0.6.0",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@altimateai/altimate-core": "0.
|
|
20
|
+
"@altimateai/altimate-core": "0.3.0"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@altimateai/altimate-code-linux-x64-musl": "0.
|
|
24
|
-
"@altimateai/altimate-code-linux-x64-baseline": "0.
|
|
25
|
-
"@altimateai/altimate-code-windows-arm64": "0.
|
|
26
|
-
"@altimateai/altimate-code-darwin-arm64": "0.
|
|
27
|
-
"@altimateai/altimate-code-windows-x64-baseline": "0.
|
|
28
|
-
"@altimateai/altimate-code-linux-arm64": "0.
|
|
29
|
-
"@altimateai/altimate-code-linux-x64-baseline-musl": "0.
|
|
30
|
-
"@altimateai/altimate-code-darwin-x64-baseline": "0.
|
|
31
|
-
"@altimateai/altimate-code-windows-x64": "0.
|
|
32
|
-
"@altimateai/altimate-code-darwin-x64": "0.
|
|
33
|
-
"@altimateai/altimate-code-linux-x64": "0.
|
|
34
|
-
"@altimateai/altimate-code-linux-arm64-musl": "0.
|
|
23
|
+
"@altimateai/altimate-code-linux-x64-musl": "0.6.0",
|
|
24
|
+
"@altimateai/altimate-code-linux-x64-baseline": "0.6.0",
|
|
25
|
+
"@altimateai/altimate-code-windows-arm64": "0.6.0",
|
|
26
|
+
"@altimateai/altimate-code-darwin-arm64": "0.6.0",
|
|
27
|
+
"@altimateai/altimate-code-windows-x64-baseline": "0.6.0",
|
|
28
|
+
"@altimateai/altimate-code-linux-arm64": "0.6.0",
|
|
29
|
+
"@altimateai/altimate-code-linux-x64-baseline-musl": "0.6.0",
|
|
30
|
+
"@altimateai/altimate-code-darwin-x64-baseline": "0.6.0",
|
|
31
|
+
"@altimateai/altimate-code-windows-x64": "0.6.0",
|
|
32
|
+
"@altimateai/altimate-code-darwin-x64": "0.6.0",
|
|
33
|
+
"@altimateai/altimate-code-linux-x64": "0.6.0",
|
|
34
|
+
"@altimateai/altimate-code-linux-arm64-musl": "0.6.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"pg": ">=8",
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-parity
|
|
3
|
+
description: Validate that two tables or query results are identical — or diagnose exactly how they differ. Discover schema, identify keys, profile cheaply, then diff. Use for migration validation, ETL regression, and query refactor verification.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Data Parity (Table Diff)
|
|
7
|
+
|
|
8
|
+
## CRITICAL: Regulated / Sensitive Data
|
|
9
|
+
|
|
10
|
+
`data_diff` includes up to 5 **sample diff rows** in the tool output so you can see *which* values differ. Those rows are part of the conversation and are sent to the LLM provider you're using.
|
|
11
|
+
|
|
12
|
+
Before running `data_diff` against a table that might contain PII, PHI, PCI, or other regulated data:
|
|
13
|
+
|
|
14
|
+
1. **Ask the user** whether the target contains regulated columns.
|
|
15
|
+
2. If yes, prefer `algorithm: "profile"` — it compares column-level statistics (count, nulls, min/max, distinct count) without any row values leaving the database.
|
|
16
|
+
3. If a row-level diff is genuinely required, tell the user that up to 5 sample rows will be sent to the LLM and get explicit approval before calling the tool.
|
|
17
|
+
4. Consider scoping with `where_clause` to exclude sensitive customers/accounts first.
|
|
18
|
+
|
|
19
|
+
Default to profile mode whenever the table name suggests regulated data (`customers`, `patients`, `orders`, `payments`, `accounts`, `users`, etc.) unless the user explicitly requests row-level comparison.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## CRITICAL: Always Start With a Plan
|
|
24
|
+
|
|
25
|
+
**Before doing anything else**, generate a numbered TODO list for the user:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Here's my plan:
|
|
29
|
+
1. [ ] List available warehouse connections
|
|
30
|
+
2. [ ] Inspect schema, discover primary key candidates, and detect auto-timestamp columns
|
|
31
|
+
3. [ ] Confirm primary keys with you
|
|
32
|
+
4. [ ] Confirm which auto-timestamp columns to exclude
|
|
33
|
+
5. [ ] Check row counts on both sides
|
|
34
|
+
6. [ ] Run column-level profile (cheap — no row scan)
|
|
35
|
+
7. [ ] Ask whether to proceed with row-level diff (may be expensive for large tables)
|
|
36
|
+
8. [ ] Run targeted row-level diff on diverging columns only
|
|
37
|
+
9. [ ] Present findings with scope, filters, time period, columns compared/excluded, and assumptions
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Update each item to `[x]` as you complete it. This plan should be visible before any tool is called.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## CRITICAL: Use `data_diff` Tool — Never Write Manual Diff SQL
|
|
45
|
+
|
|
46
|
+
**NEVER** write SQL to diff tables manually (e.g., `EXCEPT`, `FULL OUTER JOIN`, `MINUS`).
|
|
47
|
+
**ALWAYS** use the `data_diff` tool for any comparison operation.
|
|
48
|
+
|
|
49
|
+
`sql_query` is only for:
|
|
50
|
+
- Schema inspection (`information_schema`, `SHOW COLUMNS`, `DESCRIBE`)
|
|
51
|
+
- Cardinality checks to identify keys
|
|
52
|
+
- Row count estimates
|
|
53
|
+
|
|
54
|
+
Everything else — profile, row diff, value comparison — goes through `data_diff`.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Step 1: List Connections
|
|
59
|
+
|
|
60
|
+
Use `warehouse_list` to show the user what connections are available and which warehouses map to source and target.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Step 2: Inspect Schema, Discover Primary Keys, and Detect Auto-Timestamp Columns
|
|
65
|
+
|
|
66
|
+
Use `sql_query` to get columns, defaults, and identify key candidates:
|
|
67
|
+
|
|
68
|
+
```sql
|
|
69
|
+
-- Postgres / Redshift / DuckDB
|
|
70
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
71
|
+
FROM information_schema.columns
|
|
72
|
+
WHERE table_schema = 'public' AND table_name = 'orders'
|
|
73
|
+
ORDER BY ordinal_position
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```sql
|
|
77
|
+
-- Snowflake
|
|
78
|
+
SHOW COLUMNS IN TABLE orders
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```sql
|
|
82
|
+
-- MySQL / MariaDB (also fetch EXTRA for ON UPDATE detection)
|
|
83
|
+
SELECT column_name, data_type, is_nullable, column_default, extra
|
|
84
|
+
FROM information_schema.columns
|
|
85
|
+
WHERE table_schema = 'mydb' AND table_name = 'orders'
|
|
86
|
+
ORDER BY ordinal_position
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```sql
|
|
90
|
+
-- SQL Server / Fabric
|
|
91
|
+
SELECT c.name AS column_name, tp.name AS data_type, c.is_nullable,
|
|
92
|
+
dc.definition AS column_default
|
|
93
|
+
FROM sys.columns c
|
|
94
|
+
INNER JOIN sys.types tp ON c.user_type_id = tp.user_type_id
|
|
95
|
+
INNER JOIN sys.objects o ON c.object_id = o.object_id
|
|
96
|
+
INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
|
|
97
|
+
LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id
|
|
98
|
+
WHERE s.name = 'dbo' AND o.name = 'orders'
|
|
99
|
+
ORDER BY c.column_id
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```sql
|
|
103
|
+
-- ClickHouse
|
|
104
|
+
DESCRIBE TABLE source_db.events
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Look for:** columns named `id`, `*_id`, `*_key`, `uuid`, or with `NOT NULL` + unique index.
|
|
108
|
+
|
|
109
|
+
**Also look for auto-timestamp columns** — any column whose `column_default` contains a time-generating function:
|
|
110
|
+
- PostgreSQL/DuckDB/Redshift: `now()`, `CURRENT_TIMESTAMP`, `clock_timestamp()`
|
|
111
|
+
- MySQL/MariaDB: `CURRENT_TIMESTAMP` (in default or EXTRA)
|
|
112
|
+
- Snowflake: `CURRENT_TIMESTAMP()`, `SYSDATE()`
|
|
113
|
+
- SQL Server: `getdate()`, `sysdatetime()`
|
|
114
|
+
- Oracle: `SYSDATE`, `SYSTIMESTAMP`
|
|
115
|
+
|
|
116
|
+
These columns auto-generate values on INSERT, so they inherently differ between source and target due to write timing — not because of actual data discrepancies. **Collect them for confirmation in Step 4.**
|
|
117
|
+
|
|
118
|
+
If no obvious PK, run a cardinality check:
|
|
119
|
+
|
|
120
|
+
```sql
|
|
121
|
+
SELECT
|
|
122
|
+
COUNT(*) AS total_rows,
|
|
123
|
+
COUNT(DISTINCT order_id) AS distinct_order_id,
|
|
124
|
+
COUNT(DISTINCT customer_id) AS distinct_customer_id
|
|
125
|
+
FROM orders
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A valid key column: `distinct_count = total_rows`.
|
|
129
|
+
|
|
130
|
+
For composite keys:
|
|
131
|
+
```sql
|
|
132
|
+
SELECT order_id, line_item_id, COUNT(*) AS cnt
|
|
133
|
+
FROM order_lines
|
|
134
|
+
GROUP BY order_id, line_item_id
|
|
135
|
+
HAVING COUNT(*) > 1
|
|
136
|
+
LIMIT 5
|
|
137
|
+
```
|
|
138
|
+
If this returns 0 rows, `(order_id, line_item_id)` is a valid composite key.
|
|
139
|
+
|
|
140
|
+
## Step 3: Confirm Keys With the User
|
|
141
|
+
|
|
142
|
+
**Always confirm** the identified key columns before proceeding:
|
|
143
|
+
|
|
144
|
+
> "I identified `order_id` as the primary key (150,000 distinct values = 150,000 rows, no NULLs). Does that look right, or should I use a different column?"
|
|
145
|
+
|
|
146
|
+
Do not proceed to diff until the user confirms or corrects.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Step 4: Confirm Auto-Timestamp Column Exclusions
|
|
151
|
+
|
|
152
|
+
If you detected any columns with auto-generating timestamp defaults in Step 2, **present them to the user and ask for confirmation** before excluding them.
|
|
153
|
+
|
|
154
|
+
**Example prompt when auto-timestamp columns are found:**
|
|
155
|
+
|
|
156
|
+
> "I found **3 columns** with auto-generating timestamp defaults that will inherently differ between source and target (due to when each row was written, not actual data differences):
|
|
157
|
+
>
|
|
158
|
+
> | Column | Default | Reason to exclude |
|
|
159
|
+
> |--------|---------|-------------------|
|
|
160
|
+
> | `created_at` | `DEFAULT now()` | Set on insert — reflects when this copy was written |
|
|
161
|
+
> | `updated_at` | `DEFAULT now()` | Set on insert — reflects when this copy was written |
|
|
162
|
+
> | `_loaded_at` | `DEFAULT CURRENT_TIMESTAMP` | ETL load timestamp |
|
|
163
|
+
>
|
|
164
|
+
> Should I **exclude** these from the comparison? Or do you want to include any of them (e.g., if you're verifying that `created_at` was preserved during migration)?"
|
|
165
|
+
|
|
166
|
+
**If user confirms exclusion:** Omit those columns from `extra_columns` when calling `data_diff`.
|
|
167
|
+
|
|
168
|
+
**If user wants to include some:** Add them explicitly to `extra_columns`.
|
|
169
|
+
|
|
170
|
+
**If no auto-timestamp columns were detected:** Skip this step and proceed to Step 5.
|
|
171
|
+
|
|
172
|
+
> **Why ask?** In migration validation, `created_at` should often be *identical* between source and target (it was migrated, not regenerated). But in ETL replication, `created_at` is freshly generated on each side and *should* differ. Only the user knows which case applies.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Step 5: Check Row Counts
|
|
177
|
+
|
|
178
|
+
```sql
|
|
179
|
+
SELECT COUNT(*) FROM orders -- run on both source and target
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Use counts to:
|
|
183
|
+
- Detect load completeness issues before row-level diff
|
|
184
|
+
- Choose the algorithm and decide whether to ask about cost
|
|
185
|
+
- If counts differ significantly (>5%), flag it immediately
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Step 6: Column-Level Profile (Always Run This First)
|
|
190
|
+
|
|
191
|
+
Profile is cheap — it runs aggregates, not row scans. **Always run profile before row-level diff.**
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
data_diff(
|
|
195
|
+
source="orders",
|
|
196
|
+
target="orders",
|
|
197
|
+
key_columns=["order_id"],
|
|
198
|
+
source_warehouse="postgres_prod",
|
|
199
|
+
target_warehouse="snowflake_dw",
|
|
200
|
+
algorithm="profile"
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Profile tells you:
|
|
205
|
+
- Row count on each side
|
|
206
|
+
- Which columns have null count differences → NULL handling bug
|
|
207
|
+
- Min/max divergence per column → value transformation bug
|
|
208
|
+
- Which columns match exactly → safe to skip in row-level diff
|
|
209
|
+
|
|
210
|
+
**Example output:**
|
|
211
|
+
```
|
|
212
|
+
Column Profile Comparison
|
|
213
|
+
|
|
214
|
+
✓ order_id: match
|
|
215
|
+
✓ customer_id: match
|
|
216
|
+
✗ amount: DIFFER ← source min=10.00, target min=10.01 — rounding?
|
|
217
|
+
✗ status: DIFFER ← source nulls=0, target nulls=47 — NULL mapping bug?
|
|
218
|
+
✓ created_at: match
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Step 7: Ask Before Running Row-Level Diff on Large Tables
|
|
224
|
+
|
|
225
|
+
After profiling, check row count and **ask the user** before proceeding:
|
|
226
|
+
|
|
227
|
+
**If table has < 100K rows:** proceed automatically.
|
|
228
|
+
|
|
229
|
+
**If table has 100K–10M rows:**
|
|
230
|
+
> "The table has 1.2M rows. Row-level diff will scan all rows on both sides — this may take 30–60 seconds and consume warehouse compute. Do you want to proceed? You can also provide a `where_clause` to limit the scope (e.g., `created_at >= '2024-01-01'`)."
|
|
231
|
+
|
|
232
|
+
**If table has > 10M rows:**
|
|
233
|
+
> "The table has 50M rows. Full row-level diff could be expensive. Options:
|
|
234
|
+
> 1. Diff a recent window only (e.g., last 30 days)
|
|
235
|
+
> 2. Partition by a date/key column — shows which partition has problems without scanning everything
|
|
236
|
+
> 3. Proceed with full diff (may take several minutes)
|
|
237
|
+
> Which would you prefer?"
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Step 8: Run Targeted Row-Level Diff
|
|
242
|
+
|
|
243
|
+
Use only the columns that the profile said differ. This is faster and produces cleaner output.
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
data_diff(
|
|
247
|
+
source="orders",
|
|
248
|
+
target="orders",
|
|
249
|
+
key_columns=["order_id"],
|
|
250
|
+
extra_columns=["amount", "status"], // only diverging columns from profile
|
|
251
|
+
source_warehouse="postgres_prod",
|
|
252
|
+
target_warehouse="snowflake_dw",
|
|
253
|
+
algorithm="hashdiff"
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### For large tables — use partition_column
|
|
258
|
+
|
|
259
|
+
Split the table into groups and diff each independently. Three modes:
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
// Date column — partition by month
|
|
263
|
+
data_diff(source="lineitem", target="lineitem",
|
|
264
|
+
key_columns=["l_orderkey", "l_linenumber"],
|
|
265
|
+
source_warehouse="pg_source", target_warehouse="pg_target",
|
|
266
|
+
partition_column="l_shipdate", partition_granularity="month",
|
|
267
|
+
algorithm="hashdiff")
|
|
268
|
+
|
|
269
|
+
// Numeric column — partition by key ranges of 100K
|
|
270
|
+
data_diff(source="orders", target="orders",
|
|
271
|
+
key_columns=["o_orderkey"],
|
|
272
|
+
source_warehouse="pg_source", target_warehouse="pg_target",
|
|
273
|
+
partition_column="o_orderkey", partition_bucket_size=100000,
|
|
274
|
+
algorithm="hashdiff")
|
|
275
|
+
|
|
276
|
+
// Categorical column — partition by distinct values (string, enum, boolean)
|
|
277
|
+
data_diff(source="orders", target="orders",
|
|
278
|
+
key_columns=["o_orderkey"],
|
|
279
|
+
source_warehouse="pg_source", target_warehouse="pg_target",
|
|
280
|
+
partition_column="o_orderstatus",
|
|
281
|
+
algorithm="hashdiff")
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Output includes aggregate diff + per-partition breakdown showing which group has problems.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Algorithm Selection
|
|
289
|
+
|
|
290
|
+
| Algorithm | When to use |
|
|
291
|
+
|-----------|-------------|
|
|
292
|
+
| `profile` | **Always run first** — column stats (count, min, max, nulls). No row scan. |
|
|
293
|
+
| `joindiff` | Same database — single FULL OUTER JOIN. Fast, exact. |
|
|
294
|
+
| `hashdiff` | Cross-database or large tables — bisection with checksums. Scales to billions. |
|
|
295
|
+
| `cascade` | Auto-escalate: profile → hashdiff on diverging columns. |
|
|
296
|
+
| `auto` | JoinDiff if same warehouse, HashDiff if cross-database. |
|
|
297
|
+
|
|
298
|
+
> **CRITICAL:** If `source_warehouse` ≠ `target_warehouse`, **never use `joindiff`** — it only sees one connection and always reports 0 differences. Use `hashdiff` or `auto`.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Output Interpretation
|
|
303
|
+
|
|
304
|
+
### IDENTICAL
|
|
305
|
+
```
|
|
306
|
+
✓ Tables are IDENTICAL
|
|
307
|
+
Rows checked: 1,000,000
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### DIFFER
|
|
311
|
+
```
|
|
312
|
+
✗ Tables DIFFER
|
|
313
|
+
|
|
314
|
+
Source rows: 150,000
|
|
315
|
+
Target rows: 149,950
|
|
316
|
+
Only in source: 50 → rows deleted in target (ETL missed deletes)
|
|
317
|
+
Only in target: 0
|
|
318
|
+
Updated rows: 0
|
|
319
|
+
Identical rows: 149,950
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
| Pattern | Root cause |
|
|
323
|
+
|---------|-----------|
|
|
324
|
+
| `only_in_source > 0`, target = 0 | ETL dropped rows — check filters, incremental logic |
|
|
325
|
+
| `only_in_target > 0`, source = 0 | Target has extra rows — dedup issue or wrong join |
|
|
326
|
+
| `updated_rows > 0`, counts match | Silent value corruption — check type casts, rounding |
|
|
327
|
+
| Row counts differ significantly | Load completeness — check ETL watermarks |
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## CRITICAL: `extra_columns` Behavior
|
|
332
|
+
|
|
333
|
+
The Rust engine **only compares columns listed in `extra_columns`**. If the list is empty, it compares key existence only — rows that match on key but differ in values will be silently reported as "identical". This is the most common source of false positives.
|
|
334
|
+
|
|
335
|
+
**Auto-discovery (default for table names):** When `extra_columns` is omitted and the source is a plain table name, `data_diff` auto-discovers all non-key columns from the database catalog and excludes columns using two detection layers:
|
|
336
|
+
|
|
337
|
+
1. **Name-pattern matching** — columns named like `updated_at`, `created_at`, `inserted_at`, `modified_at`, `publisher_last_updated_epoch_ms`, ETL metadata columns like `_fivetran_synced`, `_airbyte_extracted_at`, etc.
|
|
338
|
+
2. **Schema-level default detection** — columns with auto-generating timestamp defaults (`DEFAULT NOW()`, `DEFAULT CURRENT_TIMESTAMP`, `GETDATE()`, `SYSDATE()`, `SYSTIMESTAMP`, etc.), detected directly from the database catalog. This catches columns that don't follow naming conventions but still auto-generate values on INSERT. Works across PostgreSQL, MySQL, Snowflake, SQL Server, Oracle, ClickHouse, DuckDB, SQLite, and Redshift.
|
|
339
|
+
|
|
340
|
+
The output lists which columns were auto-excluded and why.
|
|
341
|
+
|
|
342
|
+
**SQL queries:** When source is a SQL query (not a table name), auto-discovery cannot work. You **must** provide `extra_columns` explicitly. If you don't, only key-level matching occurs.
|
|
343
|
+
|
|
344
|
+
**When to override auto-exclusion:** If the user specifically wants to compare audit columns (e.g., verifying that `created_at` was preserved during migration), pass those columns explicitly in `extra_columns`.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Step 9: Present Findings — Always Surface Context
|
|
349
|
+
|
|
350
|
+
When reporting diff results, **never present bare numbers**. Always frame the result with the full context that determines what the numbers actually mean.
|
|
351
|
+
|
|
352
|
+
### Required elements in every result summary
|
|
353
|
+
|
|
354
|
+
**1. Scope — what was compared**
|
|
355
|
+
State exactly which tables/queries were diffed and on which warehouses:
|
|
356
|
+
> "Compared `public.orders` on **postgres_prod** vs `public.orders` on **snowflake_dw**"
|
|
357
|
+
|
|
358
|
+
**2. Filters and time period applied**
|
|
359
|
+
If any `where_clause` or `partition_column` was used, state it explicitly:
|
|
360
|
+
> "Scope limited to: `created_at >= '2024-01-01' AND created_at < '2024-04-01'` (Q1 2024 only)"
|
|
361
|
+
> "Partitioned by `l_shipdate` (monthly buckets) — diff covers Jan 2023 through Mar 2024"
|
|
362
|
+
|
|
363
|
+
If no filter was applied, say so:
|
|
364
|
+
> "No row filter applied — full table compared"
|
|
365
|
+
|
|
366
|
+
**3. Key columns used**
|
|
367
|
+
> "Key: `order_id` (confirmed unique — 150,000 distinct values = 150,000 rows)"
|
|
368
|
+
|
|
369
|
+
**4. Columns included and excluded**
|
|
370
|
+
List what was compared and what was skipped, and why:
|
|
371
|
+
> "Compared columns: `amount`, `status`, `customer_id`"
|
|
372
|
+
> "Excluded (auto-timestamp defaults): `created_at`, `updated_at`, `_loaded_at`"
|
|
373
|
+
> "Excluded (user request): `internal_score`"
|
|
374
|
+
|
|
375
|
+
If the user confirmed exclusions in Step 4, reference that confirmation:
|
|
376
|
+
> "Excluded per your confirmation: `created_at`, `updated_at`"
|
|
377
|
+
|
|
378
|
+
**5. Algorithm used**
|
|
379
|
+
> "Algorithm: `hashdiff` (cross-database)"
|
|
380
|
+
|
|
381
|
+
### Example full result summary
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
## Data Parity Results
|
|
385
|
+
|
|
386
|
+
**Compared:** `public.orders` (postgres_prod) → `public.orders` (snowflake_dw)
|
|
387
|
+
**Scope:** `created_at >= '2024-01-01'` (Q1 2024 only — 42,301 rows in scope)
|
|
388
|
+
**Key:** `order_id`
|
|
389
|
+
**Columns compared:** `amount`, `status`, `customer_id`, `region`
|
|
390
|
+
**Columns excluded:** `created_at`, `updated_at` (auto-timestamp defaults, per your confirmation)
|
|
391
|
+
**Algorithm:** hashdiff
|
|
392
|
+
|
|
393
|
+
### Result: ✗ DIFFER
|
|
394
|
+
|
|
395
|
+
| Metric | Value |
|
|
396
|
+
|--------|-------|
|
|
397
|
+
| Source rows | 42,301 |
|
|
398
|
+
| Target rows | 42,298 |
|
|
399
|
+
| Only in source | 3 |
|
|
400
|
+
| Only in target | 0 |
|
|
401
|
+
| Updated rows | 47 |
|
|
402
|
+
| Identical rows | 42,251 |
|
|
403
|
+
|
|
404
|
+
**Findings:**
|
|
405
|
+
- 3 rows exist in source but are missing in target → possible ETL delete propagation gap
|
|
406
|
+
- 47 rows have value differences in `amount` or `status` → check rounding or status mapping
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### When result is IDENTICAL — still surface the scope
|
|
410
|
+
|
|
411
|
+
Even when tables match perfectly, state what was checked:
|
|
412
|
+
> "✓ Tables are **identical** across 150,000 rows. Compared `amount`, `status`, `customer_id` (full table, no filter, key=`order_id`). Auto-timestamp columns `created_at`, `updated_at` were excluded."
|
|
413
|
+
|
|
414
|
+
**Why this matters:** "Tables are identical" without context is meaningless — the user needs to know if you checked Q1 only, skipped 5 columns, or used a WHERE clause that covered just 1% of the data.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Common Mistakes
|
|
419
|
+
|
|
420
|
+
**Writing manual diff SQL instead of calling data_diff**
|
|
421
|
+
→ Never use EXCEPT, MINUS, or FULL OUTER JOIN to diff tables. Use `data_diff`.
|
|
422
|
+
|
|
423
|
+
**Calling data_diff without confirming the key**
|
|
424
|
+
→ Confirm cardinality with the user first. A bad key gives meaningless results.
|
|
425
|
+
|
|
426
|
+
**Using joindiff for cross-database tables**
|
|
427
|
+
→ JoinDiff can't see the remote table. Always returns 0 diffs. Use `hashdiff` or `auto`.
|
|
428
|
+
|
|
429
|
+
**Skipping the profile step and jumping to full row diff**
|
|
430
|
+
→ Profile is free. It tells you which columns actually differ so you avoid scanning everything.
|
|
431
|
+
|
|
432
|
+
**Running full diff on a billion-row table without asking**
|
|
433
|
+
→ Always ask the user before expensive operations. Offer filtering and partition options.
|
|
434
|
+
|
|
435
|
+
**Omitting extra_columns when source is a SQL query**
|
|
436
|
+
→ Auto-discovery only works for table names. For SQL queries, always list the columns to compare explicitly.
|
|
437
|
+
|
|
438
|
+
**Silently excluding auto-timestamp columns without asking the user**
|
|
439
|
+
→ Always present detected auto-timestamp columns (Step 4) and get explicit confirmation. In migration scenarios, `created_at` should be *identical* — excluding it silently hides real bugs.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## SQL Server and Microsoft Fabric
|
|
444
|
+
|
|
445
|
+
### Minimum Version Requirements
|
|
446
|
+
|
|
447
|
+
| Component | Minimum Version | Why |
|
|
448
|
+
|---|---|---|
|
|
449
|
+
| **SQL Server** | 2022 (16.x) | `DATETRUNC()` used for date partitioning; `LEAST()`/`GREATEST()` used by Rust engine |
|
|
450
|
+
| **Azure SQL Database** | Any current version | Always has `DATETRUNC()` and `LEAST()` |
|
|
451
|
+
| **Microsoft Fabric** | Any current version | T-SQL surface includes all required functions |
|
|
452
|
+
| **mssql** (npm) | 12.0.0 | `ConnectionPool` isolation for concurrent connections, tedious 19 |
|
|
453
|
+
| **@azure/identity** (npm) | 4.0.0 | Required only for Azure AD authentication; tedious imports it internally |
|
|
454
|
+
|
|
455
|
+
> **Note:** Date partitioning (`partition_column` + `partition_granularity`) uses `DATETRUNC()` which is **not available on SQL Server 2019 or earlier**. Basic diff operations (joindiff, hashdiff, profile) work on older versions. If you need partitioned diffs on SQL Server < 2022, use numeric or categorical partitioning instead.
|
|
456
|
+
|
|
457
|
+
### Supported Configurations
|
|
458
|
+
|
|
459
|
+
| Warehouse Type | Authentication | Notes |
|
|
460
|
+
|---|---|---|
|
|
461
|
+
| `sqlserver` / `mssql` | User/password or Azure AD | On-prem or Azure SQL. SQL Server 2022+ required for date partitioning. |
|
|
462
|
+
| `fabric` | Azure AD only | Microsoft Fabric SQL endpoint. Always uses TLS encryption. |
|
|
463
|
+
|
|
464
|
+
### Connecting to Microsoft Fabric
|
|
465
|
+
|
|
466
|
+
Fabric uses the same TDS protocol as SQL Server — no separate driver needed. Configuration:
|
|
467
|
+
|
|
468
|
+
```yaml
|
|
469
|
+
type: "fabric"
|
|
470
|
+
host: "<workspace-id>-<item-id>.datawarehouse.fabric.microsoft.com"
|
|
471
|
+
database: "<warehouse-name>"
|
|
472
|
+
authentication: "azure-active-directory-default" # recommended
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Auth shorthands (mapped to full tedious type names):
|
|
476
|
+
- `CLI` or `default` → `azure-active-directory-default`
|
|
477
|
+
- `password` → `azure-active-directory-password`
|
|
478
|
+
- `service-principal` → `azure-active-directory-service-principal-secret`
|
|
479
|
+
- `msi` or `managed-identity` → `azure-active-directory-msi-vm`
|
|
480
|
+
|
|
481
|
+
Full Azure AD authentication types:
|
|
482
|
+
- `azure-active-directory-default` — auto-discovers credentials via `DefaultAzureCredential` (recommended; works with `az login`)
|
|
483
|
+
- `azure-active-directory-password` — username/password with `azure_client_id` and `azure_tenant_id`
|
|
484
|
+
- `azure-active-directory-access-token` — pre-obtained token (does **not** auto-refresh)
|
|
485
|
+
- `azure-active-directory-service-principal-secret` — service principal with `azure_client_id`, `azure_client_secret`, `azure_tenant_id`
|
|
486
|
+
- `azure-active-directory-msi-vm` / `azure-active-directory-msi-app-service` — managed identity
|
|
487
|
+
|
|
488
|
+
### Algorithm Behavior
|
|
489
|
+
|
|
490
|
+
- **Same-warehouse** MSSQL or Fabric → `joindiff` (single FULL OUTER JOIN, most efficient)
|
|
491
|
+
- **Cross-warehouse** MSSQL/Fabric ↔ other database → `hashdiff` (automatic when using `auto`)
|
|
492
|
+
- The Rust engine maps `sqlserver`/`mssql` to `tsql` dialect and `fabric` to `fabric` dialect — both generate valid T-SQL syntax with bracket quoting (`[schema].[table]`).
|