How much of a technology migration is judgment, and how much is labor? This experiment ports a working DuckDB analytics platform to Snowflake, proves the output equivalent to the source row by row, and counts which parts actually needed a person. Four did.
A migration is a good test case for how far an AI-assisted workflow can carry a task on its own, because all the ambiguity is already gone. The source platform is a complete, executable statement of the right answer: the Snowflake version should compute exactly what DuckDB already computes. The target is documented and finite. Nothing in the middle is a research question.
So my prediction was specific enough to be wrong. If what historically made migrations expensive was line-by-line translation labor, then this port should come out mostly autonomous, the gaps that surfaced should be mechanical rather than structural, and the result should be provably equivalent to the source rather than merely plausible. If instead the gaps forced redesign decisions, the prediction fails and the interesting constraint is somewhere else.
A faithful port, deliberately. No re-architecture, no improvements in flight. The scope was the platinum-marts DAG: 65 models across bronze, silver (raw vault), gold (business vault plus dimensional), and platinum. Orphan models nothing downstream consumed were deferred rather than ported, and the tests attached to them were excluded rather than counted as failures.
Bootstrap on the Snowflake side was four scripts: account and
schema setup, an external stage with a Parquet file format, a
PUT per source across 16 mappings, and an
INFER_SCHEMA plus COPY per source.
Runtime was the local dbt-snowflake adapter
against a trial account over key-pair auth.
Equivalence was not judged by eye. A match contract ran inside Snowflake at three tiers: grain, then typed content row by row, then a raw-vault spot check. Matching is on natural keys and content, never on hashes, because the two engines produce different hash representations for identical inputs and a hash comparison would report a difference that is not one.
| Measure | Result |
|---|---|
| Build on Snowflake | 65 models, 468 tests, 0 errors, 0 failures |
Files changed under dbt/models/ |
0 |
| Parity, grain | PASS |
| Parity, typed content | PASS — 0 mismatches across 180,533 mart rows and 3,222 dimension rows |
| Parity, raw-vault spot check | PASS — 5 of 5 tables exact |
| Dialect gaps surfaced | 4, none of which changed output |
The number that surprised me was zero. Not one file under
dbt/models/ was modified. The DuckDB DAG and the
Snowflake DAG are byte-identical. Every portability problem got
fixed outside the models, in bootstrap SQL or in a single
dispatched macro.
That is a result about how the platform was built rather than about either engine. The models encode business logic; the dialect-specific parts had already been pushed to the edges, into adapter dispatch and source resolution. So the surface that had to move was the plumbing, and the logic stayed put.
Every gap is listed, including the one the first fix caused. The last column is the one that matters: a gap that changes output is a redesign problem wearing a syntax costume, and none of these did.
| # | Symptom | Cause | Fix | Output changed |
|---|---|---|---|---|
| 1 | 18 bronze models failed with invalid identifier "GEOFIPS" |
INFER_SCHEMA preserved the Parquet files' lowercase column names as case-sensitive identifiers. The models reference them unquoted, so the parser uppercases the reference and the match fails. |
Uppercase the column names in the INFER_SCHEMA template. Bootstrap side. |
No |
| 2 | One table broke because of fix 1 | Its source columns contain spaces, so they must be quoted, and quoting makes them case-sensitive. The blanket uppercase rule broke the one table it could not apply to. | Exempt that table and keep its native case. | No |
| 3 | dim_date would not build |
Snowflake's GENERATOR requires a constant rowcount and rejects the date expression the DuckDB branch used. |
Rewrite the Snowflake branch of the date-spine macro as a fixed span bounded by a WHERE. Output-identical; DuckDB branch untouched. |
No |
| 4 | Selection and seed artifacts | Two objects needed explicit selection, and 10 foreign-key tests pointed at the orphan models deferred from scope. | Explicit selection; excluded the tests belonging to models deliberately not ported. | No |
Gap 1 had a correct-looking mechanical fix and a better one. Snowflake stores identifiers uppercase by default, and mixed-case identifiers degrade micro-partition pruning, which is a large part of what makes Snowflake fast. So uppercasing was not just the way to make the error go away; it was the native choice for the target. DuckDB never cared about case. The rule I took from it: port toward the target's grain rather than carrying the source's habits across.
Gap 2 is the correction to gap 1. One table cannot take the uppercase rule, and a blanket rule would have quietly broken it. Migration rules are almost uniform, and the residue is where the judgment lives.
One focused session, start to finish: re-entry, probes, scope, green build, parity proven, wrap. Infrastructure cost was a Snowflake trial credit against an extra-small warehouse on a small dataset, which rounds to nothing at this size.
The dominant cost was neither the porting nor the compute. It was review bandwidth: reading diffs, making the case-versus- pruning call, deciding which orphan models to leave behind. Four decisions genuinely needed a person. Everything else was mechanical translation that ran on its own.
That ratio is the finding I care about. A handful of judgment calls sitting on top of a large volume of autonomous translation means the throughput limit is how fast a human can verify, and staffing the work as though translation were the bottleneck would size it wrong.
This was a lift and shift on public data at small scale. It is not evidence for any of the following, and I have not run them:
The one thing that aged badly was a runtime decision made during scoping. By the time the build ran, the researched choice was stale and got revised mid-flight. Cheap to fix, but it says something about sequencing: research done at scoping time has a shelf life, and the build report rather than the spec ends up holding the corrected truth.
Next time I would date the technology choices in the spec and re-check them at build time rather than trusting them because they were written down.
The full build history, feature specifications, structured
build reports, bootstrap SQL, and the parity contract live in
the project repository. The data-quality page renders the three
reconciliation tiers directly from the build reports rather
than from anything hand-written.
gazetteer.danbrickey.dev