migrate-prove 0.1.0__tar.gz
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.
- migrate_prove-0.1.0/LICENSE +21 -0
- migrate_prove-0.1.0/PKG-INFO +239 -0
- migrate_prove-0.1.0/README.md +204 -0
- migrate_prove-0.1.0/pyproject.toml +52 -0
- migrate_prove-0.1.0/setup.cfg +4 -0
- migrate_prove-0.1.0/src/migrate_prove/__init__.py +5 -0
- migrate_prove-0.1.0/src/migrate_prove/__main__.py +4 -0
- migrate_prove-0.1.0/src/migrate_prove/checks.py +774 -0
- migrate_prove-0.1.0/src/migrate_prove/cli.py +110 -0
- migrate_prove-0.1.0/src/migrate_prove/connectors.py +218 -0
- migrate_prove-0.1.0/src/migrate_prove/contract.py +23 -0
- migrate_prove-0.1.0/src/migrate_prove/demo/__init__.py +5 -0
- migrate_prove-0.1.0/src/migrate_prove/demo/banking.py +158 -0
- migrate_prove-0.1.0/src/migrate_prove/demo/cte_api.py +185 -0
- migrate_prove-0.1.0/src/migrate_prove/demo/hops_vs_e2e.py +96 -0
- migrate_prove-0.1.0/src/migrate_prove/engine.py +142 -0
- migrate_prove-0.1.0/src/migrate_prove/eval.py +63 -0
- migrate_prove-0.1.0/src/migrate_prove/hashing.py +15 -0
- migrate_prove-0.1.0/src/migrate_prove/models.py +245 -0
- migrate_prove-0.1.0/src/migrate_prove/normalize.py +47 -0
- migrate_prove-0.1.0/src/migrate_prove/reporting.py +434 -0
- migrate_prove-0.1.0/src/migrate_prove/rules.py +55 -0
- migrate_prove-0.1.0/src/migrate_prove/salesforce.py +232 -0
- migrate_prove-0.1.0/src/migrate_prove/sampling.py +54 -0
- migrate_prove-0.1.0/src/migrate_prove/secrets.py +171 -0
- migrate_prove-0.1.0/src/migrate_prove/sql.py +58 -0
- migrate_prove-0.1.0/src/migrate_prove.egg-info/PKG-INFO +239 -0
- migrate_prove-0.1.0/src/migrate_prove.egg-info/SOURCES.txt +37 -0
- migrate_prove-0.1.0/src/migrate_prove.egg-info/dependency_links.txt +1 -0
- migrate_prove-0.1.0/src/migrate_prove.egg-info/entry_points.txt +2 -0
- migrate_prove-0.1.0/src/migrate_prove.egg-info/requires.txt +18 -0
- migrate_prove-0.1.0/src/migrate_prove.egg-info/top_level.txt +1 -0
- migrate_prove-0.1.0/tests/test_contract.py +19 -0
- migrate_prove-0.1.0/tests/test_cte_api.py +111 -0
- migrate_prove-0.1.0/tests/test_engine_demo.py +41 -0
- migrate_prove-0.1.0/tests/test_hashing.py +17 -0
- migrate_prove-0.1.0/tests/test_hops_vs_e2e.py +39 -0
- migrate_prove-0.1.0/tests/test_reporting.py +142 -0
- migrate_prove-0.1.0/tests/test_secrets.py +105 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dan Webb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: migrate-prove
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Prove that migrated target data matches the Source-to-Target Mapping contract — not just that the ETL job exited 0.
|
|
5
|
+
Author: Dan Webb
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/4pconsulting/migrate-prove
|
|
8
|
+
Project-URL: Repository, https://github.com/4pconsulting/migrate-prove
|
|
9
|
+
Project-URL: Issues, https://github.com/4pconsulting/migrate-prove/issues
|
|
10
|
+
Keywords: data-migration,etl,reconciliation,qa,validation
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: pydantic>=2.7
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Requires-Dist: python-dotenv>=1.0
|
|
23
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
24
|
+
Requires-Dist: typer>=0.12
|
|
25
|
+
Requires-Dist: rich>=13.7
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
Provides-Extra: excel
|
|
29
|
+
Requires-Dist: openpyxl>=3.1; extra == "excel"
|
|
30
|
+
Provides-Extra: postgres
|
|
31
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "postgres"
|
|
32
|
+
Provides-Extra: athena
|
|
33
|
+
Requires-Dist: pyathena[sqlalchemy]>=3.0; extra == "athena"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# migrate-prove
|
|
37
|
+
|
|
38
|
+
A Python validation suite for data migrations. It does **not** treat a green ETL exit code as success. It proves that the **target dataset is the result of applying an agreed Source-to-Target Mapping (STM) to the source**, then that the target is still internally coherent.
|
|
39
|
+
|
|
40
|
+
This repository is the executable version of a five-layer migration test strategy: contract-as-oracle, risk-calibrated checks, row hashing, failure-mode probes, and an evidence scorecard.
|
|
41
|
+
|
|
42
|
+
## Why this exists
|
|
43
|
+
|
|
44
|
+
A completed pipeline answers: *did the job finish?*
|
|
45
|
+
|
|
46
|
+
This suite answers: **can we systematically prove the target represents the source correctly, according to the contract and business invariants?**
|
|
47
|
+
|
|
48
|
+
`COUNT(*)` matching on both sides is not that proof. The banking demo plants a status misclassification that keeps totals identical and still fails sliced volume, hashing, and invariants.
|
|
49
|
+
|
|
50
|
+
## Architecture
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
STM YAML (oracle)
|
|
54
|
+
│
|
|
55
|
+
▼
|
|
56
|
+
┌─────────────────────────────────────┐
|
|
57
|
+
│ ValidationEngine │
|
|
58
|
+
│ 1 Schema 2 Volume 3 Metrics │
|
|
59
|
+
│ 4 Transforms HASH 5 Invariants │
|
|
60
|
+
│ + failure-mode probes │
|
|
61
|
+
└─────────────────────────────────────┘
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
JSON + HTML evidence scorecard
|
|
65
|
+
tagged: migration | transformation | reconciliation
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### The contract is the test oracle
|
|
69
|
+
|
|
70
|
+
Target correctness is **not** `source == target`. Different can be right (status `A` → `Active`). The STM file is the shared, versioned specification:
|
|
71
|
+
|
|
72
|
+
| Dimension | How the suite tests it |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| Direct projections | Canonicalise, then compare / hash |
|
|
75
|
+
| Transformations | Lookup tables and `when`/`then` rules run against source, asserted on target |
|
|
76
|
+
| Type coercions | Decimal scale, string length, identifier quoting |
|
|
77
|
+
| Null / empty / `N/A` | Failure probe `null_empty_literal` |
|
|
78
|
+
| Dedup / filters | Optional `filter_source` / `filter_target` |
|
|
79
|
+
| Cardinality | Total counts **and** slices (`volume_slices`) |
|
|
80
|
+
| Surrogate keys | Join on mapped business keys, not physical PKs |
|
|
81
|
+
|
|
82
|
+
### Five layers (+ hash + probes)
|
|
83
|
+
|
|
84
|
+
| Layer | Question | Typical concern tag |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| L1 Schema | Do mapped objects/columns exist? | migration |
|
|
87
|
+
| Probes | Truncation, precision, encoding, leading zeros | transformation |
|
|
88
|
+
| L2 Volume | Totals and slices after applying STM lookups | migration / transformation |
|
|
89
|
+
| L3 Metrics | Sums and grouped financials within tolerance | reconciliation |
|
|
90
|
+
| L4 Transforms | Every STM rule vs actual target value | transformation |
|
|
91
|
+
| HASH | Missing/extra keys vs row SHA-256 | migration / transformation |
|
|
92
|
+
| L5 Invariants | Temporal order, orphans, state machines | reconciliation |
|
|
93
|
+
|
|
94
|
+
Separating those tags is how you debug cutover night: dropped rows vs bad lookup vs legacy junk copied faithfully.
|
|
95
|
+
|
|
96
|
+
### Risk calibration
|
|
97
|
+
|
|
98
|
+
Each entity has `risk: critical | master | high_volume`. Critical failures are called out on the HTML scorecard. Sampling policy (`full` or `stratified` with `where` + `rate`) is on the entity so high-volume logs are not brute-forced during a four-hour window. v1 hashing still runs in-process (SQLite/demo scale); SQL-pushdown hashing is the next connector increment.
|
|
99
|
+
|
|
100
|
+
## Install
|
|
101
|
+
|
|
102
|
+
```powershell
|
|
103
|
+
pip install migrate-prove
|
|
104
|
+
pip install "migrate-prove[postgres]"
|
|
105
|
+
pip install "migrate-prove[athena]"
|
|
106
|
+
pip install "migrate-prove[excel]"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`postgres` installs the psycopg driver, `athena` installs the PyAthena SQLAlchemy dialect, and `excel` installs openpyxl. The demos and their YAML live in this repository, so clone it and follow the quick start below to seed and run them.
|
|
110
|
+
|
|
111
|
+
## Releasing
|
|
112
|
+
|
|
113
|
+
Bump `version` in `pyproject.toml` in the pull request. Merging that change to `main` publishes the new version to PyPI. A push that does not change the version does not upload again.
|
|
114
|
+
|
|
115
|
+
## Quick start
|
|
116
|
+
|
|
117
|
+
Ensure you are in the root `migrate-prove` directory.
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
python -m venv .venv
|
|
121
|
+
.venv\Scripts\Activate.ps1
|
|
122
|
+
python -m pip install --upgrade pip
|
|
123
|
+
python -m pip install -e ".[dev]"
|
|
124
|
+
migrate-prove seed-demo examples/banking_demo
|
|
125
|
+
migrate-prove compile examples/banking_demo/stm.yaml
|
|
126
|
+
migrate-prove run examples/banking_demo/suite.yaml
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Open `examples/banking_demo/artifacts/report.html`. You should see a **passing total count** and failing sliced volume, transforms, hashes, probes, and invariants — that is the point.
|
|
130
|
+
|
|
131
|
+
### Hops vs end-to-end
|
|
132
|
+
|
|
133
|
+
Layered pipelines need both thin hop gates and a finalised source → target STM. Runnable demo (hops pass; e2e fails on status misclassification):
|
|
134
|
+
|
|
135
|
+
```powershell
|
|
136
|
+
migrate-prove seed-hops-demo examples/hops_vs_e2e
|
|
137
|
+
migrate-prove run examples/hops_vs_e2e/suites/hop_01.yaml
|
|
138
|
+
migrate-prove run examples/hops_vs_e2e/suites/hop_02.yaml
|
|
139
|
+
migrate-prove run examples/hops_vs_e2e/suites/e2e.yaml
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
See [examples/hops_vs_e2e/README.md](examples/hops_vs_e2e/README.md) and [ASSURANCE.md](ASSURANCE.md).
|
|
143
|
+
|
|
144
|
+
### CTE source + Salesforce (mock) target
|
|
145
|
+
|
|
146
|
+
Not every migration is table→table. A third demo joins two source tables via a **CTE** and proves against **paginated Salesforce JSON fixtures**:
|
|
147
|
+
|
|
148
|
+
```powershell
|
|
149
|
+
migrate-prove seed-cte-api-demo examples/cte_api_demo
|
|
150
|
+
migrate-prove run examples/cte_api_demo/suite.yaml
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
See [examples/cte_api_demo/README.md](examples/cte_api_demo/README.md) and [ADVANCED.md](ADVANCED.md).
|
|
154
|
+
|
|
155
|
+
```powershell
|
|
156
|
+
pytest
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Connections and secrets
|
|
160
|
+
|
|
161
|
+
Suite YAML must **never** contain passwords. Reference environment variables with `${VAR}` or `${VAR:-default}`. Locally, copy [`.env.example`](.env.example) to `.env` (gitignored). In CI, set the same names from the pipeline secrets store — `python-dotenv` only fills gaps; real process env always wins.
|
|
162
|
+
|
|
163
|
+
**URL form** (any SQLAlchemy URL after expansion):
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
source:
|
|
167
|
+
url: "postgresql+psycopg://${SOURCE_USER}:${SOURCE_PASSWORD}@${SOURCE_HOST}:${SOURCE_PORT:-5432}/${SOURCE_DB}"
|
|
168
|
+
target:
|
|
169
|
+
url: "${TARGET_URL}"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Structured form** (builds the URL; same `${}` rules):
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
source:
|
|
176
|
+
dialect: postgresql+psycopg
|
|
177
|
+
host: ${SOURCE_HOST}
|
|
178
|
+
port: ${SOURCE_PORT:-5432}
|
|
179
|
+
database: ${SOURCE_DB}
|
|
180
|
+
username: ${SOURCE_USER}
|
|
181
|
+
password: ${SOURCE_PASSWORD}
|
|
182
|
+
options:
|
|
183
|
+
sslmode: require
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Install drivers as extras when needed:
|
|
187
|
+
|
|
188
|
+
```powershell
|
|
189
|
+
pip install -e ".[postgres]" # psycopg
|
|
190
|
+
pip install -e ".[athena]" # PyAthena SQLAlchemy dialect
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Athena stays URL-shaped; AWS credentials come from the environment or the runner IAM role:
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
target:
|
|
197
|
+
url: "awsathena+rest://athena.${AWS_REGION}.amazonaws.com:443/${ATHENA_SCHEMA}?s3_staging_dir=${ATHENA_S3_STAGING}&work_group=${ATHENA_WORKGROUP}"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Optional: `migrate-prove run suite.yaml --env-file path\to\.env`. The CLI prints **redacted** connection labels (SQLAlchemy URLs or `salesforce_mock://…` fixture paths).
|
|
201
|
+
|
|
202
|
+
**API mock target** (no network; see [ADVANCED.md](ADVANCED.md)):
|
|
203
|
+
|
|
204
|
+
```yaml
|
|
205
|
+
target:
|
|
206
|
+
kind: salesforce_mock
|
|
207
|
+
fixtures: fixtures/salesforce
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Authoring a contract
|
|
211
|
+
|
|
212
|
+
See `examples/banking_demo/stm.yaml`. A suite file points at the contract and the two connections (SQLite needs no secrets):
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
name: my-cutover
|
|
216
|
+
contract: stm.yaml
|
|
217
|
+
source: { url: sqlite:///source.db }
|
|
218
|
+
target: { url: "${TARGET_URL}" }
|
|
219
|
+
output_dir: artifacts
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Transform rules are the oracle, not a second copy of ETL Python. Keep ETL and this file aligned; do not reimplement warehouse SQL ad hoc in tests.
|
|
223
|
+
|
|
224
|
+
## Roadmap (v1 is the skeleton that already fails the right way)
|
|
225
|
+
|
|
226
|
+
1. **Now** — STM YAML, SQLAlchemy + Salesforce-mock connectors, CTE `source_sql`, env-backed secrets, L1–L5, hashing, probes, CLI, HTML/JSON, demos.
|
|
227
|
+
2. **Excel STM import** — optional extra `openpyxl` to ingest the spreadsheet everyone still has.
|
|
228
|
+
3. **Dialect hash pushdown** — `SHA2` / `sha256` in Postgres, SQL Server, Snowflake so row signatures never leave the estate.
|
|
229
|
+
4. **Stratified sampling at warehouse scale** — deterministic modulus on business keys, 100% for HNW / recent / edge strata.
|
|
230
|
+
5. **JUnit XML + CI gate** — cutover sign-off as a pipeline artifact.
|
|
231
|
+
6. **Rehearsal profiles** — synthetic → prod-replica → dress-rehearsal, with runtime budgets.
|
|
232
|
+
|
|
233
|
+
## Design rules
|
|
234
|
+
|
|
235
|
+
- The STM is executable. If it cannot be checked, it is not specified.
|
|
236
|
+
- Prefer evidence over exit codes.
|
|
237
|
+
- Hunt known failure modes on purpose; do not only sample happy-path rows.
|
|
238
|
+
- Never claim success from a global row count.
|
|
239
|
+
- Never commit secrets; suite YAML references `${ENV}` only.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# migrate-prove
|
|
2
|
+
|
|
3
|
+
A Python validation suite for data migrations. It does **not** treat a green ETL exit code as success. It proves that the **target dataset is the result of applying an agreed Source-to-Target Mapping (STM) to the source**, then that the target is still internally coherent.
|
|
4
|
+
|
|
5
|
+
This repository is the executable version of a five-layer migration test strategy: contract-as-oracle, risk-calibrated checks, row hashing, failure-mode probes, and an evidence scorecard.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
A completed pipeline answers: *did the job finish?*
|
|
10
|
+
|
|
11
|
+
This suite answers: **can we systematically prove the target represents the source correctly, according to the contract and business invariants?**
|
|
12
|
+
|
|
13
|
+
`COUNT(*)` matching on both sides is not that proof. The banking demo plants a status misclassification that keeps totals identical and still fails sliced volume, hashing, and invariants.
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
STM YAML (oracle)
|
|
19
|
+
│
|
|
20
|
+
▼
|
|
21
|
+
┌─────────────────────────────────────┐
|
|
22
|
+
│ ValidationEngine │
|
|
23
|
+
│ 1 Schema 2 Volume 3 Metrics │
|
|
24
|
+
│ 4 Transforms HASH 5 Invariants │
|
|
25
|
+
│ + failure-mode probes │
|
|
26
|
+
└─────────────────────────────────────┘
|
|
27
|
+
│
|
|
28
|
+
▼
|
|
29
|
+
JSON + HTML evidence scorecard
|
|
30
|
+
tagged: migration | transformation | reconciliation
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### The contract is the test oracle
|
|
34
|
+
|
|
35
|
+
Target correctness is **not** `source == target`. Different can be right (status `A` → `Active`). The STM file is the shared, versioned specification:
|
|
36
|
+
|
|
37
|
+
| Dimension | How the suite tests it |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| Direct projections | Canonicalise, then compare / hash |
|
|
40
|
+
| Transformations | Lookup tables and `when`/`then` rules run against source, asserted on target |
|
|
41
|
+
| Type coercions | Decimal scale, string length, identifier quoting |
|
|
42
|
+
| Null / empty / `N/A` | Failure probe `null_empty_literal` |
|
|
43
|
+
| Dedup / filters | Optional `filter_source` / `filter_target` |
|
|
44
|
+
| Cardinality | Total counts **and** slices (`volume_slices`) |
|
|
45
|
+
| Surrogate keys | Join on mapped business keys, not physical PKs |
|
|
46
|
+
|
|
47
|
+
### Five layers (+ hash + probes)
|
|
48
|
+
|
|
49
|
+
| Layer | Question | Typical concern tag |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| L1 Schema | Do mapped objects/columns exist? | migration |
|
|
52
|
+
| Probes | Truncation, precision, encoding, leading zeros | transformation |
|
|
53
|
+
| L2 Volume | Totals and slices after applying STM lookups | migration / transformation |
|
|
54
|
+
| L3 Metrics | Sums and grouped financials within tolerance | reconciliation |
|
|
55
|
+
| L4 Transforms | Every STM rule vs actual target value | transformation |
|
|
56
|
+
| HASH | Missing/extra keys vs row SHA-256 | migration / transformation |
|
|
57
|
+
| L5 Invariants | Temporal order, orphans, state machines | reconciliation |
|
|
58
|
+
|
|
59
|
+
Separating those tags is how you debug cutover night: dropped rows vs bad lookup vs legacy junk copied faithfully.
|
|
60
|
+
|
|
61
|
+
### Risk calibration
|
|
62
|
+
|
|
63
|
+
Each entity has `risk: critical | master | high_volume`. Critical failures are called out on the HTML scorecard. Sampling policy (`full` or `stratified` with `where` + `rate`) is on the entity so high-volume logs are not brute-forced during a four-hour window. v1 hashing still runs in-process (SQLite/demo scale); SQL-pushdown hashing is the next connector increment.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
pip install migrate-prove
|
|
69
|
+
pip install "migrate-prove[postgres]"
|
|
70
|
+
pip install "migrate-prove[athena]"
|
|
71
|
+
pip install "migrate-prove[excel]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`postgres` installs the psycopg driver, `athena` installs the PyAthena SQLAlchemy dialect, and `excel` installs openpyxl. The demos and their YAML live in this repository, so clone it and follow the quick start below to seed and run them.
|
|
75
|
+
|
|
76
|
+
## Releasing
|
|
77
|
+
|
|
78
|
+
Bump `version` in `pyproject.toml` in the pull request. Merging that change to `main` publishes the new version to PyPI. A push that does not change the version does not upload again.
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
Ensure you are in the root `migrate-prove` directory.
|
|
83
|
+
|
|
84
|
+
```powershell
|
|
85
|
+
python -m venv .venv
|
|
86
|
+
.venv\Scripts\Activate.ps1
|
|
87
|
+
python -m pip install --upgrade pip
|
|
88
|
+
python -m pip install -e ".[dev]"
|
|
89
|
+
migrate-prove seed-demo examples/banking_demo
|
|
90
|
+
migrate-prove compile examples/banking_demo/stm.yaml
|
|
91
|
+
migrate-prove run examples/banking_demo/suite.yaml
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Open `examples/banking_demo/artifacts/report.html`. You should see a **passing total count** and failing sliced volume, transforms, hashes, probes, and invariants — that is the point.
|
|
95
|
+
|
|
96
|
+
### Hops vs end-to-end
|
|
97
|
+
|
|
98
|
+
Layered pipelines need both thin hop gates and a finalised source → target STM. Runnable demo (hops pass; e2e fails on status misclassification):
|
|
99
|
+
|
|
100
|
+
```powershell
|
|
101
|
+
migrate-prove seed-hops-demo examples/hops_vs_e2e
|
|
102
|
+
migrate-prove run examples/hops_vs_e2e/suites/hop_01.yaml
|
|
103
|
+
migrate-prove run examples/hops_vs_e2e/suites/hop_02.yaml
|
|
104
|
+
migrate-prove run examples/hops_vs_e2e/suites/e2e.yaml
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
See [examples/hops_vs_e2e/README.md](examples/hops_vs_e2e/README.md) and [ASSURANCE.md](ASSURANCE.md).
|
|
108
|
+
|
|
109
|
+
### CTE source + Salesforce (mock) target
|
|
110
|
+
|
|
111
|
+
Not every migration is table→table. A third demo joins two source tables via a **CTE** and proves against **paginated Salesforce JSON fixtures**:
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
migrate-prove seed-cte-api-demo examples/cte_api_demo
|
|
115
|
+
migrate-prove run examples/cte_api_demo/suite.yaml
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
See [examples/cte_api_demo/README.md](examples/cte_api_demo/README.md) and [ADVANCED.md](ADVANCED.md).
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
pytest
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Connections and secrets
|
|
125
|
+
|
|
126
|
+
Suite YAML must **never** contain passwords. Reference environment variables with `${VAR}` or `${VAR:-default}`. Locally, copy [`.env.example`](.env.example) to `.env` (gitignored). In CI, set the same names from the pipeline secrets store — `python-dotenv` only fills gaps; real process env always wins.
|
|
127
|
+
|
|
128
|
+
**URL form** (any SQLAlchemy URL after expansion):
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
source:
|
|
132
|
+
url: "postgresql+psycopg://${SOURCE_USER}:${SOURCE_PASSWORD}@${SOURCE_HOST}:${SOURCE_PORT:-5432}/${SOURCE_DB}"
|
|
133
|
+
target:
|
|
134
|
+
url: "${TARGET_URL}"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Structured form** (builds the URL; same `${}` rules):
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
source:
|
|
141
|
+
dialect: postgresql+psycopg
|
|
142
|
+
host: ${SOURCE_HOST}
|
|
143
|
+
port: ${SOURCE_PORT:-5432}
|
|
144
|
+
database: ${SOURCE_DB}
|
|
145
|
+
username: ${SOURCE_USER}
|
|
146
|
+
password: ${SOURCE_PASSWORD}
|
|
147
|
+
options:
|
|
148
|
+
sslmode: require
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Install drivers as extras when needed:
|
|
152
|
+
|
|
153
|
+
```powershell
|
|
154
|
+
pip install -e ".[postgres]" # psycopg
|
|
155
|
+
pip install -e ".[athena]" # PyAthena SQLAlchemy dialect
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Athena stays URL-shaped; AWS credentials come from the environment or the runner IAM role:
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
target:
|
|
162
|
+
url: "awsathena+rest://athena.${AWS_REGION}.amazonaws.com:443/${ATHENA_SCHEMA}?s3_staging_dir=${ATHENA_S3_STAGING}&work_group=${ATHENA_WORKGROUP}"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Optional: `migrate-prove run suite.yaml --env-file path\to\.env`. The CLI prints **redacted** connection labels (SQLAlchemy URLs or `salesforce_mock://…` fixture paths).
|
|
166
|
+
|
|
167
|
+
**API mock target** (no network; see [ADVANCED.md](ADVANCED.md)):
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
target:
|
|
171
|
+
kind: salesforce_mock
|
|
172
|
+
fixtures: fixtures/salesforce
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Authoring a contract
|
|
176
|
+
|
|
177
|
+
See `examples/banking_demo/stm.yaml`. A suite file points at the contract and the two connections (SQLite needs no secrets):
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
name: my-cutover
|
|
181
|
+
contract: stm.yaml
|
|
182
|
+
source: { url: sqlite:///source.db }
|
|
183
|
+
target: { url: "${TARGET_URL}" }
|
|
184
|
+
output_dir: artifacts
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Transform rules are the oracle, not a second copy of ETL Python. Keep ETL and this file aligned; do not reimplement warehouse SQL ad hoc in tests.
|
|
188
|
+
|
|
189
|
+
## Roadmap (v1 is the skeleton that already fails the right way)
|
|
190
|
+
|
|
191
|
+
1. **Now** — STM YAML, SQLAlchemy + Salesforce-mock connectors, CTE `source_sql`, env-backed secrets, L1–L5, hashing, probes, CLI, HTML/JSON, demos.
|
|
192
|
+
2. **Excel STM import** — optional extra `openpyxl` to ingest the spreadsheet everyone still has.
|
|
193
|
+
3. **Dialect hash pushdown** — `SHA2` / `sha256` in Postgres, SQL Server, Snowflake so row signatures never leave the estate.
|
|
194
|
+
4. **Stratified sampling at warehouse scale** — deterministic modulus on business keys, 100% for HNW / recent / edge strata.
|
|
195
|
+
5. **JUnit XML + CI gate** — cutover sign-off as a pipeline artifact.
|
|
196
|
+
6. **Rehearsal profiles** — synthetic → prod-replica → dress-rehearsal, with runtime budgets.
|
|
197
|
+
|
|
198
|
+
## Design rules
|
|
199
|
+
|
|
200
|
+
- The STM is executable. If it cannot be checked, it is not specified.
|
|
201
|
+
- Prefer evidence over exit codes.
|
|
202
|
+
- Hunt known failure modes on purpose; do not only sample happy-path rows.
|
|
203
|
+
- Never claim success from a global row count.
|
|
204
|
+
- Never commit secrets; suite YAML references `${ENV}` only.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "migrate-prove"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Prove that migrated target data matches the Source-to-Target Mapping contract — not just that the ETL job exited 0."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Dan Webb" }]
|
|
14
|
+
keywords = ["data-migration", "etl", "reconciliation", "qa", "validation"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Software Development :: Testing",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pydantic>=2.7",
|
|
25
|
+
"pyyaml>=6.0",
|
|
26
|
+
"python-dotenv>=1.0",
|
|
27
|
+
"sqlalchemy>=2.0",
|
|
28
|
+
"typer>=0.12",
|
|
29
|
+
"rich>=13.7",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = ["pytest>=8.0"]
|
|
34
|
+
excel = ["openpyxl>=3.1"]
|
|
35
|
+
postgres = ["psycopg[binary]>=3.1"]
|
|
36
|
+
athena = ["pyathena[sqlalchemy]>=3.0"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/4pconsulting/migrate-prove"
|
|
40
|
+
Repository = "https://github.com/4pconsulting/migrate-prove"
|
|
41
|
+
Issues = "https://github.com/4pconsulting/migrate-prove/issues"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
migrate-prove = "migrate_prove.cli:app"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
pythonpath = ["src"]
|
|
52
|
+
addopts = "-q"
|