Stage 3: rewrite as Lakeflow Spark Declarative Pipelines - #3
Open
p17b wants to merge 7 commits into
Open
Conversation
Replace the ingest/transform/aggregate jobs with a single pipeline declaring three datasets. Execution order comes from the table references in the code rather than from running jobs in sequence, and the runtime owns checkpoints, triggers, output modes, and the writes themselves. The transformation logic is untouched: both branches import the same transform_silver and build_gold from the windmill wheel, covered by the same 43 unit tests. Only orchestration differs, which is what makes them comparable. Bronze and silver are streaming tables. Gold is a materialized view rather than a streaming table, because streaming tables never revisit rows they have already emitted -- a daily aggregate built as one would go stale the moment late or corrected data reached silver. It reads silver with spark.read, not readStream: a streaming read would impose watermark and state constraints for no benefit, since the anomaly baseline spans the whole history rather than a window. Every expectation is expect_all -- warn and keep, never _or_drop or _or_fail. That is the declarative form of a decision made on the first branch: invalid readings are flagged, not discarded. Dropping them hides sensor faults from the people who fix them, and failing the update would take the pipeline down over a few bad rows in an otherwise good delivery. Silver's conditions reference the boolean columns the transform already produces instead of restating the rules, so the rule cannot drift between column and expectation. Expectation metrics reproduce the fixture manifest independently: 27 bad turbine ids, 9 bad power readings, 10 bad wind speeds, 8 bad bearings, 20 unknown-group rows -- summing to the manifest's 54 invalid readings. Gold's three expectations pass 465/465. Results match the job branches exactly: silver 11169, 54 invalid, 20 unknown, 465 turbine-days, 20 anomalies, and the stage 1 validation job passes unchanged against pipeline-produced tables. One migration constraint found the hard way: a pipeline cannot adopt an existing managed table. The first run failed because the job branches had written plain Delta tables at those names; they had to be dropped so the pipeline could own them. Harmless for regenerable fixtures, but on a real migration it means a cutover plan rather than a switch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add three sections to the README. Implementation branches -- what each of the three stages adds, a comparison across orchestration, testability, quality reporting, table ownership, migration cost and portability, and the note that all three produce identical results with the stage 1 validation job passing unchanged on each. That last part is what makes the comparison fair rather than three separate claims. Environments and isolation -- how one name_prefix line per target separates schemas as well as job names, why that makes end-to-end testing safe (a service principal can exercise ingestion through assertions with no blast radius on real data), and how a preprod target for full-volume performance and quality testing is a target block rather than an architecture change. Write-Audit-Publish -- the pattern the target isolation enables. Write and Audit are implemented here; Publish is explicitly not, since there is no production target to promote into and building one would be scaffolding around a hypothetical. The shape a promotion step would take is described instead. Also records why the two audit mechanisms are kept independent: expectations report what the pipeline saw, the validation job asserts what it should have seen against an independently generated manifest, and neither can catch the other's failures. Switching between branches documents that a destroy is required first, and why: a pipeline cannot adopt an existing managed table, so job-written tables must be dropped before the pipeline can own them. Harmless for regenerable fixtures, a cutover plan on a real migration. Note the pattern's established name is Write-Audit-Publish, not Write-Ahead- Publish -- the audit gate is the middle step, and it is the one usually skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The branch comparison read as a feature matrix aimed at someone who already knew the platform. Rewrite it around what each stage was for -- make it work, make it testable, make it declarative -- with a short "best when" table instead of eight rows of implementation detail. The trade-off that actually matters (sdp writes the least code but ties you to Databricks and needs a cutover to leave) is stated plainly rather than spread across rows. Promote the DEV and TEST targets to their own section near the top. That isolation is the point of the setup: the same code runs against different data in separate schemas, so a user or service principal can exercise the pipeline end to end without impacting real data, and preprod for full-load performance and quality testing is a config block rather than an architecture change. Add four mermaid diagrams: branch progression, environment isolation, the Write-Audit-Publish flow, and the pipeline DAG replacing the ASCII art. GitHub renders these inline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The task asks for a brief description of the solution design and the assumptions made. The README opened straight into branch detail, so a reader had to work through several sections before reaching either. Add a TL;DR covering what was built, why it exists in three implementations, the environment separation that makes end-to-end testing safe, and the two decisions that shape everything else -- flagging invalid readings rather than dropping or imputing them, and computing anomaly baselines over daily means rather than raw readings. scripts/export_chat.py converts a Claude Code session transcript to Markdown. The raw JSONL is ~4.5 MB of interleaved prose, tool calls and tool output; verbatim it is unreadable, so prose is kept in full while tool input and output are truncated to a collapsed preview. Harness-injected blocks are stripped, and sub-agent threads are skipped. The generated docs/opus_chat.md is gitignored. It is a working record rather than part of the deliverable, and it carries the full back-and-forth instead of the conclusions -- which are what the docs are for. Regenerate it locally when wanted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs/architecture.md was written during initial scaffolding and never updated. It asserted behaviour the pipeline does not have and, in two places, the exact opposite of the documented design: Silver Layer: Forward-fill missing values (by turbine) Silver Layer: Outlier removal (>2 std dev from mean) Both were deliberately rejected -- forward-filling invents measurements that were never taken, and removing outliers deletes the deliverable the task asks for. It also listed an efficiency score that was never implemented, and an assumption that sensor gaps are forward-fillable. A reader would have concluded the pipeline does the reverse of what the README argues for. Same class of problem as the dead src/ package removed earlier: documentation drifting into a claim about behaviour. Rewritten around the decisions and what was rejected, since the README already covers what it does and how to run it. Adds the silver/gold split rationale (a check belongs in silver if one row can judge it), the sqrt(24) baseline defect and its fix, per-turbine rather than fleet-wide baselines, the >= 0 boundary, why gold is a materialized view rather than a streaming table, and an honest known- gaps section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third of three stages, stacked on
medallion_whl.Replaces the ingest / transform / aggregate jobs with a single pipeline that
declares three datasets. Execution order is derived from table references in the
code; the runtime owns checkpoints, triggers, output modes, and the writes.
The transformation logic is unchanged. Both branches import the same
transform_silverandbuild_goldfrom the wheel, covered by the same 43 unittests. Only orchestration differs — which is what makes the two comparable, and
what the wheel from stage 2 bought us.
What disappears
saveAsTable+overwriteSchemaThree job YAMLs and three notebooks deleted; three transformation files and one
pipeline YAML added.
Dataset types
Bronze and silver are streaming tables — append-only, incremental.
Gold is a materialized view, deliberately. Streaming tables never revisit
rows they have already emitted, so a daily aggregate built as one would go stale
the moment late or corrected data reached silver. It reads silver with
spark.read, notreadStream: a streaming read would impose watermark and stateconstraints for no benefit, since the anomaly baseline spans the whole history
rather than a window.
Expectations
Every one is
expect_all— warn and keep, never_or_dropor_or_fail. Thatis the declarative form of a decision made on stage 1: invalid readings are
flagged, not discarded. Dropping them hides sensor faults from the people who
fix them; failing the update would take the pipeline down over a few bad rows in
an otherwise good delivery.
Silver's conditions reference the boolean columns the transform already produces
rather than restating the rules in SQL — one source of truth, so the rule cannot
drift between column and expectation.
The metrics reproduce the fixture manifest independently:
turbine_id_in_expected_grouppower_output_present_and_non_negativewind_speed_present_and_non_negativewind_direction_within_compass_rangesource_file_maps_to_known_groupSumming to the manifest's 54 invalid readings. Two mechanisms, completely
different routes, same numbers. Gold's three expectations pass 465/465.
Results
Identical to the job branches: silver 11,169 / 54 invalid / 20 UNKNOWN / 465
turbine-days / 20 anomalies. The stage 1 validation job passes unchanged
against pipeline-produced tables — a fair cross-check rather than a rewritten
test.
Migration constraint
The first run failed:
A pipeline cannot adopt an existing managed table. The job branches had
written plain Delta tables at those names, and they had to be dropped so the
pipeline could own them. Harmless for regenerable fixtures; on a real migration
it means a cutover plan — write to new names and swap, or accept a rebuild
window.
Trade-offs
For: far less orchestration code; dependency graph derived rather than
maintained; quality is a queryable per-run artifact; table types force an
explicit answer to "recompute or append?".
Against: the pipeline owns its tables, so migration is a cutover; debugging
moves from reading a notebook top-to-bottom to reading a graph plus an event
log; and it is Databricks-specific in a way that plain PySpark plus a scheduler
is not.
Neither is strictly better. Declarative wins when the shape is stable and
quality reporting matters; the job version is easier to reason about while the
logic is still moving, and it ports.
See
docs/declarative_pipeline.mdfor the full write-up.