|
1 | 1 | # SQLCompare |
2 | 2 |
|
3 | | -Professional CLI for comparing datasets across database tables. |
| 3 | +SQLCompare helps you understand how a change impacted your data. |
| 4 | +When you modify logic, filters, or inputs, SQLCompare lets you compare the previous and current versions of a dataset—whether they come from tables, SQL queries, or files. |
4 | 5 |
|
5 | | -SQLCompare performs row-level comparisons between a "previous" and "current" dataset using a composite key, materializes a join table in a comparison schema, computes column-level differences, and stores a diff run ID for repeatable analysis. |
| 6 | +You can compare datasets in two complementary ways: |
6 | 7 |
|
7 | | -## Key Features |
8 | | -- Compare tables or dataset queries using composite keys. |
9 | | -- Persist comparison runs with metadata for later analysis. |
10 | | -- Generate per-column stats, missing-row checks, and samples of differences. |
11 | | -- Designed for repeatable CLI workflows in data engineering and QA. |
| 8 | +1) Row-by-row comparison (with an ID): detect missing rows on either side, identify the columns with the most changes, and inspect before/after values for any record. |
| 9 | +2) Statistical comparison: compare column-level statistics such as null counts, distinct counts, and other aggregates to quickly understand overall impact. |
12 | 10 |
|
13 | | -## Installation |
14 | | -Use your project setup and dependency manager of choice. For local development: |
| 11 | +--- |
| 12 | + |
| 13 | +## What you get |
| 14 | + |
| 15 | +- **Repeatable checks** for releases, backfills, migrations, and vendor drops |
| 16 | +- **Clear summaries**: missing-row detection + per-column change counts |
| 17 | +- **One workflow** for warehouses *and* local files (DuckDB-powered) |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | +## Install |
| 22 | + |
| 23 | +Recommended: |
15 | 24 |
|
16 | 25 | ```bash |
17 | | -uv sync --extra dev |
| 26 | +uv tool install sqlcompare |
18 | 27 | ``` |
19 | 28 |
|
20 | | -## Quick Start |
21 | | -Compare two tables on a single key: |
| 29 | +Install optional connector extras as needed: |
22 | 30 |
|
23 | 31 | ```bash |
24 | | -sqlcompare table analytics.fact_sales analytics.fact_sales_new id |
| 32 | +uv tool install "sqlcompare[<connector>]" |
25 | 33 | ``` |
26 | 34 |
|
27 | | -Compare using a composite key: |
| 35 | +Examples: |
28 | 36 |
|
29 | 37 | ```bash |
30 | | -sqlcompare table analytics.users analytics.users_new user_id,tenant_id |
| 38 | +uv tool install "sqlcompare[snowflake]" |
| 39 | +uv tool install "sqlcompare[databricks]" |
31 | 40 | ``` |
32 | 41 |
|
33 | | -Use a named connector and custom schema: |
| 42 | +--- |
| 43 | + |
| 44 | +## Quick start (tables) |
| 45 | + |
| 46 | +Compare two tables on a key: |
34 | 47 |
|
35 | 48 | ```bash |
36 | | -sqlcompare table public.orders public.orders_latest order_id -c prod --schema sqlcompare |
| 49 | +export SQLCOMPARE_CONN_DEFAULT="postgresql://<user>:<pass>@<host>/<db>" |
| 50 | +sqlcompare table analytics.fact_sales analytics.fact_sales_new id |
37 | 51 | ``` |
38 | 52 |
|
39 | | -Follow up with analysis: |
| 53 | +That command prints a **diff_id**. Use it for follow-up analysis: |
40 | 54 |
|
41 | 55 | ```bash |
42 | | -sqlcompare report <diff_id> --stats |
43 | | -sqlcompare report <diff_id> --column revenue --limit 100 |
44 | | -sqlcompare report <diff_id> --missing-current |
| 56 | +sqlcompare analyze-diff <diff_id> --stats |
| 57 | +sqlcompare analyze-diff <diff_id> --column revenue --limit 100 |
| 58 | +sqlcompare analyze-diff <diff_id> --missing-current |
45 | 59 | ``` |
46 | 60 |
|
47 | | -## Command Reference |
| 61 | +--- |
| 62 | + |
| 63 | +## Core idea: compare once, analyze many times |
| 64 | + |
| 65 | +SQLCompare does two things: |
| 66 | + |
| 67 | +1. **Compare** two datasets using an index (single or composite key) |
| 68 | +2. Persist comparison results and return a **diff_id** you can use to: |
| 69 | + |
| 70 | +* get overall stats |
| 71 | +* drill into a specific column’s changes |
| 72 | +* list missing rows (previous-only / current-only) |
| 73 | +* pull samples for debugging |
48 | 74 |
|
49 | | -### Compare tables |
| 75 | +--- |
| 76 | + |
| 77 | +## Usage by use case |
| 78 | + |
| 79 | +### 1) Compare two tables |
| 80 | + |
| 81 | +Best for production validation and regression checks across supported connectors. |
50 | 82 |
|
51 | 83 | ```bash |
52 | | -sqlcompare table TABLE1 TABLE2 IDS [--connection/-c CONNECTION] [--schema SCHEMA] |
| 84 | +sqlcompare table analytics.users analytics.users_new user_id,tenant_id |
53 | 85 | ``` |
54 | 86 |
|
55 | | -#### Arguments |
56 | | -- `TABLE1`: Fully qualified name of the "previous" table to compare. Passed directly to SQL (example: `analytics.schema.table`). |
57 | | -- `TABLE2`: Fully qualified name of the "current" table to compare. Passed directly to SQL. |
58 | | -- `IDS`: Comma-separated list of columns that uniquely identify a row (example: `id` or `id,sub_id`). Whitespace around commas is stripped. |
| 87 | +Why it’s useful: |
59 | 88 |
|
60 | | -#### Options |
61 | | -- `--connection`, `-c`: Name of a connector profile to use. If omitted, SQLCompare reads the default connector from configuration. |
62 | | -- `--schema`: Schema used to store the join table created for the comparison. If omitted, it uses the configured comparison schema. |
| 89 | +* Handles **composite keys** |
| 90 | +* Produces a **saved diff ID** for repeatable review |
63 | 91 |
|
64 | | -## Configuration |
65 | | -SQLCompare is configured through environment variables: |
| 92 | +--- |
| 93 | + |
| 94 | +### 2) Compare SQL query results |
| 95 | + |
| 96 | +Use this when tables aren’t materialized yet or you want a filtered slice. |
| 97 | + |
| 98 | +Create a dataset config: |
| 99 | + |
| 100 | +```yaml |
| 101 | +previous: |
| 102 | + select_sql: "SELECT * FROM analytics.orders WHERE order_date < '2024-01-01'" |
| 103 | + index: |
| 104 | + - ORDER_ID |
| 105 | + |
| 106 | +new: |
| 107 | + select_sql: "SELECT * FROM analytics.orders WHERE order_date >= '2024-01-01'" |
| 108 | + index: |
| 109 | + - ORDER_ID |
| 110 | +``` |
| 111 | +
|
| 112 | +Run the compare: |
| 113 | +
|
| 114 | +```bash |
| 115 | +sqlcompare dataset path/to/dataset.yaml |
| 116 | +``` |
| 117 | + |
| 118 | +Why it’s useful: |
| 119 | + |
| 120 | +* Compare slices without touching production tables |
| 121 | +* Keep an auditable **diff_id** |
66 | 122 |
|
67 | | -- `SQLCOMPARE_COMPARISON_SCHEMA`: |
68 | | - Schema used for storing comparison tables. |
69 | | -- `SQLCOMPARE_CONN_DEFAULT`: |
70 | | - Name of the default connector profile. |
71 | | -- `SQLCOMPARE_CONN_{NAME}`: |
72 | | - SQLAlchemy URL for a named connector profile. Use with `-c {NAME}`. |
| 123 | +--- |
73 | 124 |
|
74 | | -## How It Works |
| 125 | +### 3) Compare local CSV / XLSX files (DuckDB) |
75 | 126 |
|
76 | | -1) Resolve configuration, connector, and comparison schema. |
77 | | -2) Parse `IDS` into a list of index columns. |
78 | | -3) Create a unique comparison name and diff ID. |
79 | | -4) Use `DatabaseComparator` to: |
80 | | - - Verify index columns exist in both tables (case-insensitive match). |
81 | | - - Create a `FULL OUTER JOIN` table in the comparison schema. |
82 | | - - Count rows present only in either table. |
83 | | - - Compute per-column differences for shared columns. |
84 | | - - Persist metadata for later analysis. |
85 | | -5) Emit a summary and a follow-up report command. |
| 127 | +Great for ad hoc QA, one-off deliveries, or vendor drops. |
| 128 | +SQLCompare uses DuckDB under the hood — no DB server required. |
86 | 129 |
|
87 | | -## Diff IDs and Metadata |
88 | | -Each run is recorded in: |
| 130 | +Create a dataset config (supports `{{here}}` for relative paths): |
89 | 131 |
|
| 132 | +```yaml |
| 133 | +previous: |
| 134 | + file_name: "{{here}}/previous.csv" |
| 135 | + index: |
| 136 | + - id |
| 137 | + |
| 138 | +new: |
| 139 | + file_name: "{{here}}/current.xlsx" |
| 140 | + index: |
| 141 | + - id |
90 | 142 | ``` |
91 | | -~/.config/sqlcompare/db_test_runs.yaml |
| 143 | +
|
| 144 | +Set a local default connector and run: |
| 145 | +
|
| 146 | +```bash |
| 147 | +export SQLCOMPARE_CONN_DEFAULT="duckdb:///:memory:" |
| 148 | +sqlcompare dataset path/to/dataset.yaml |
92 | 149 | ``` |
93 | 150 |
|
94 | | -The record includes: |
95 | | -- `tables`: mapping of previous, current, and join table names |
96 | | -- `index_cols`: resolved index columns (as found in the database) |
97 | | -- `cols_prev` / `cols_new`: column lists for each table |
98 | | -- `conn`: connector name (or `duckdb` for file-based runs) |
| 151 | +Why it’s useful: |
| 152 | + |
| 153 | +* Same diff workflow as warehouses |
| 154 | +* Fast local comparisons, zero infra |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +### 4) Compare tables inside a DuckDB file |
99 | 159 |
|
100 | | -Diff ID format: |
| 160 | +Use this when your data lives in a local `.duckdb` file. |
101 | 161 |
|
| 162 | +```bash |
| 163 | +export SQLCOMPARE_CONN_LOCAL="duckdb:////absolute/path/to/warehouse.duckdb" |
| 164 | +sqlcompare table raw.customers staged.customers id -c local |
102 | 165 | ``` |
103 | | -compare_{table1}_{table2}_{timestamp}_{random} |
| 166 | + |
| 167 | +Why it’s useful: |
| 168 | + |
| 169 | +* Local + fast, without Snowflake/warehouse costs |
| 170 | +* Easy to integrate into lightweight pipelines |
| 171 | + |
| 172 | +--- |
| 173 | + |
| 174 | +## Configuration |
| 175 | + |
| 176 | +SQLCompare resolves connectors in this order: |
| 177 | + |
| 178 | +1. default connector |
| 179 | +2. direct URL |
| 180 | +3. environment variables |
| 181 | +4. YAML files |
| 182 | + |
| 183 | +### Environment variables |
| 184 | + |
| 185 | +* `SQLCOMPARE_CONN_DEFAULT` — default connector URL (used when `-c` is omitted) |
| 186 | +* `SQLCOMPARE_CONN_{NAME}` — SQLAlchemy URL for a named connector |
| 187 | +* `SQLCOMPARE_COMPARISON_SCHEMA` — schema for comparison tables (default: `sqlcompare`) |
| 188 | + |
| 189 | +Example: |
| 190 | + |
| 191 | +```bash |
| 192 | +export SQLCOMPARE_CONN_DEFAULT="postgresql://..." |
| 193 | +export SQLCOMPARE_CONN_LOCAL="duckdb:////abs/path/to/db.duckdb" |
104 | 194 | ``` |
105 | 195 |
|
106 | | -Table names are sanitized to alphanumeric plus underscores. |
| 196 | +### YAML connections file (optional) |
107 | 197 |
|
108 | | -## Output Summary |
109 | | -The CLI prints a concise report to stdout (via `data_toolkit.core.log`), including: |
110 | | -- Missing rows in either table (based on null key columns in the join table). |
111 | | -- Total number of value differences across common rows. |
112 | | -- Per-column difference counts (when common columns exist). |
113 | | -- A sample of the first 10 differences. |
114 | | -- A follow-up command to inspect the diff (`sqlcompare report <diff_id>`). |
| 198 | +Location: |
115 | 199 |
|
116 | | -## Difference Detection |
| 200 | +``` |
| 201 | +~/.sqlcompare/connections.yml |
| 202 | +``` |
117 | 203 |
|
118 | | -`DatabaseComparator` builds a join table with the following shape: |
119 | | -- For every column in `TABLE1`: `column_previous` |
120 | | -- For every column in `TABLE2`: `column_new` |
| 204 | +Example: |
| 205 | + |
| 206 | +```yaml |
| 207 | +snowflake: |
| 208 | + drivername: snowflake |
| 209 | + username: my_user |
| 210 | + password: my_password |
| 211 | + host: my_account |
| 212 | + database: ANALYTICS |
| 213 | + schema: PUBLIC |
| 214 | + query: |
| 215 | + warehouse: COMPUTE_WH |
| 216 | +``` |
121 | 217 |
|
122 | | -It uses a `FULL OUTER JOIN` on the provided `IDS`. |
| 218 | +--- |
123 | 219 |
|
124 | | -Differences are detected when: |
125 | | -- The row exists on both sides (key columns are not null in both). |
126 | | -- A common column does not match (or one side is `NULL` and the other is not). |
| 220 | +## Operational notes |
127 | 221 |
|
128 | | -Analysis queries are generated on-demand: |
129 | | -- `get_diff_query`: per-row differences across all common columns |
130 | | -- `get_stats_query`: counts of differences per column |
131 | | -- `get_in_current_only_query` / `get_in_previous_only_query`: missing-row checks |
| 222 | +* SQLCompare creates a **physical join table** in the comparison schema. Ensure your connector has `CREATE SCHEMA` and `CREATE TABLE` privileges. |
| 223 | +* Table names are passed through directly to SQL. Provide fully-qualified or quoted names when required by your database. |
| 224 | +* Index columns must exist in both datasets; otherwise the command fails with a clear error. |
132 | 225 |
|
133 | | -## Operational Notes |
134 | | -- The command creates a physical join table in the comparison schema. Ensure the connector has `CREATE SCHEMA` and `CREATE TABLE` privileges. |
135 | | -- Table names are passed through directly to SQL. Provide fully-qualified or quoted names when required by your database. |
136 | | -- Index columns must exist in both tables; otherwise the command fails with a clear error that includes a sample of available columns. |
| 226 | +--- |
137 | 227 |
|
138 | 228 | ## Development |
139 | 229 |
|
|
0 commit comments