feat(sql-plan): port SQLPlanGenerator consumers of the production model fields - #19
Merged
dmautz1 merged 9 commits intoAug 31, 2026
Merged
Conversation
…el fields Wire every SQL-generation field PR DocumentDrivenDX#18 landed as a declarative stub through its generator/resolver consumers: the new base_table_strategy 'union_branches' (per-source UNION branches projecting the target column set, with per-branch row_filter, union_value literals, CAST(NULL) alignment, and dedup-latest windows), base_table_filter / base_join_column / final_filter / final_dedup, ForeignKey.join_filter (candidate-level wins), and OutgoingRelationship.alternative_joins as a portable UNION-of-joins. Also: base views now project join-source columns (latent bug - emitted joins referenced columns disposition_base never selected), Excel metadata sheet JSON-encodes list values, umf.schema.json synced for the consumed fields, gold_union_branches conformance case executed on DuckDB + Spark, and a new docs/guide/sql-plans.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The final assembly and union_branches column set previously projected alphabetically by column name. The final projection defines the physical column order of a CREATE TABLE ... AS target, so emitted order must follow the spec's declared positions — alphabetical output produces a semantically different table schema than the spec declares. _output_ordered_columns(): position order first; columns without a parseable position sort after all positioned columns, alphabetically among themselves (deterministic for specs that never set positions — the previous behavior is unchanged for them since name order was already the contract). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
source_expression/target_expression on OutgoingRelationship replace the plain column equality in direct-join ON clauses (e.g. TRIM-keyed registry joins: ON TRIM(base.npi) = TRIM(target.npi)). Bare column tokens are qualified via the quote-span rewriter — the base side against the accumulated base-view columns (new explicit-columns param), the target side against the joined table's columns. Direct joins only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LookupJoin on OutgoingRelationship (source_key / bridge_table / bridge_source_key / bridge_target_key) makes a direct join reach its target THROUGH a bridge when base and target share no direct key (base.IncidentID -> bronze_incident.FacilityID -> dim_facility.facility_id). The generator emits base JOIN bridge ON base.source_key = bridge.bridge_source_key, then JOIN target ON target.<col> = bridge.bridge_target_key. Direct joins only. Model exported, JSON schema updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_extract_columns_from_expression parsed with a keyword-filtered regex that dropped all-caps tokens as keywords — an all-caps COLUMN name (QPA) was silently omitted from the base view, so a downstream expression referencing it failed qualification. Now parses the expression with sqlglot and collects real Column nodes (bare name, alias__ prefix stripped); the regex remains only as a parse-failure fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_rewrite_expression_for_alias ran the bare-identifier regex over the whole expression, so a backtick-quoted column (TRIM(`Service`)) got the base alias inserted INSIDE the backticks — `base.Service`, a literal column that does not exist. Now splits into code / backtick / string spans (like _rewrite_join_filter) and qualifies a quoted column as base.`Service`; string literals are never rewritten. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-aggregation views previously fired only under base_table_strategy= union_sources. A base-table dim with an aggregate candidate (MAX/MIN/SUM/ COUNT over a source, grouped per key) now also materializes a GROUP-BY view joined back on the base key. Three parts: - invoke _generate_pre_aggregation_views in the base_table branch; - exclude fully-aggregated sources from the regular join sequence (_aggregated_source_tables — else the source is joined twice, once fanned-out and once aggregated); a mixed agg+plain source stays a join; - add the candidate row_filter as a WHERE on the plain GROUP-BY view (dim_payer's ref_elig is WHERE is_current), and resolve the join-back to the base's real key column when the target PK is renamed (base bronze_ins_plan.ID vs target ins_plan_id: ON base.ID = agg.ins_plan_id). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dmautz1
force-pushed
the
feat/port-sql-plan-consumers
branch
from
August 31, 2026 17:50
a142af2 to
000da41
Compare
Contributor
Author
|
Rebased onto post-#18 main and deferred the SCD2 staged-recompute feature (previously commits 1500e05 + ed0fb1e): no current consumer uses |
dmautz1
marked this pull request as ready for review
August 31, 2026 18:15
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.
Summary
Second PR in the series porting production-hardened capabilities from the pulseflow fork of tablespec back upstream. Stacked on #18 (
feat/port-umf-model-deltas) — the diff includes #18's commits until it merges; review this PR's own change asfeat/port-umf-model-deltas...feat/port-sql-plan-consumers. Draft until #18 lands.#18 landed the model fields as declarative stubs; this PR wires every SQL-generation field through its
SQLPlanGenerator/RelationshipResolverconsumers, per the series convention: nothing stays a model-only no-op. As with #18, everything is adapted to this repo's structure (single-statement views, CTE-mode contract, renderer seam, engine-agnostic SQL executed verbatim on DuckDB and Spark) rather than copied — and several capabilities the fork never actually had (per-branchrow_filteron union branches, per-branch window dedup, target-schema branch projection) are new design pinned by a real downstream acceptance shape.Per-field consumer wiring
base_table_strategy: 'union_branches'(new enum value — see model note below)The base table plus each
union_base_tablesentry (falling back tosource_tables) becomes one UNION branch inside a singledisposition_baseview. Unlike the fork'sunion_base_tableshandling (which projects the base table's schema from every branch), each branch projects the target column set through that source's own derivation candidates — required for cutover shapes where sources have different columns:DerivationCandidate.row_filter→ the branch WHERE clause (the single distinct value among a branch's candidates; conflicts raiseValueError). This is how generation cutovers are expressed.DerivationCandidate.union_value→CAST(<literal> AS <type>)per branch (source discriminators); native str/int/float/bool typing preserved end-to-end.CAST(NULL AS <type>), keeping the UNION column-aligned.dedup_strategy: latest+ candidateorder_by→ per-branchROW_NUMBER() OVER (PARTITION BY <target primary_key> ORDER BY <order_by> DESC NULLS LAST) ... WHERE __rn = 1(NULLS LAST pinned — DuckDB/Spark default NULL placement diverges).union_type→UNION ALL(default) /UNION.union_exclude_base→ per-union-branchNOT EXISTSanti-join on the target primary key against the base branch's post-filter, post-dedup rows (deliberate improvement over the fork's raw-base anti-join). No primary key →ValueError(the fork skipped silently — a correctness foot-gun).union_coalesce_base→ 3-part union (base-only /COALESCE(b.c, u.c)overlap with base winning pk, meta, and union_value columns / union-only). Restricted to exactly one union table: the fork's per-table 3-part emission would double-count base rows with several union tables →ValueError.base.<col>(the branch already applied the candidate mapping under the target name).base_table_filter→ WHERE on the plain base view and on the union base branch (closing a fork gap — it never applied the filter on union paths). Warns and no-ops underunpivot/union_sources, which don't consume it.base_join_column→ overrides the inferred base join key in both resolver and base view and overwritessource_columnon relationships declared outgoing from the base table (fork parity; the field exists precisely when the auto key is wrong and declared rels carry that same wrong key — documented contract).final_filter/final_dedup→ final assembly wraps asSELECT [DISTINCT *] FROM (<assembly>) _final WHERE <filter>so the filter can reference derived aliases; applies to the synthetic (no-base) path too; still one statement → CTE-mode safe.ForeignKey.join_filter→ new resolver pass fillsJoinInfo.join_filterfrom FK metadata only where no candidate-level filter exists (candidate filters are(table, table_instance)-keyed and can disambiguate multi-instance joins, so they win). Emission machinery already existed and was conformance-tested.OutgoingRelationship.alternative_joins→ emitted as a UNION-of-joins, notON (a = b OR c = d): Spark plans OR-joins as BroadcastNestedLoopJoin. One inner-join branch per path over the distinct base keys (primary = priority 1, alternatives in declared order),UNION+ROW_NUMBERby__branch_prioritykeeps one match per key, joined back null-safely via the portable(a = b OR (a IS NULL AND b IS NULL))expansion (no<=>, no* EXCEPT, no engine hints).base_keysscansdisposition_basewhen every key is base-sourced (the fork's documented lazy-view fan-out guard), else the previous step view with a warning. Resolver validates each entry's columns exist (ValueErrorotherwise). Non-direct strategies warn and use the primary path only.Model change
UMFMetadata.base_table_strategynarrows fromstr | NonetoLiteral["union_sources", "unpivot", "union_branches"] | None. Repo-wide only the two existing values were in use.union_base_tablespresent without the strategy logs a warning and no-ops (notwarnings.warn— the repo'sfilterwarnings = errorwould hard-fail legacy-shaped fixtures; not a validation error — fork-authored UMFs must still load). Migration for fork specs: addbase_table_strategy: union_branches.Also in this PR
ON base.<key> = ...referenced columnsdisposition_basenever selected.Union Valuecolumn on the Derivations sheet (native typing, appended header so older workbooks import unchanged); Metadata sheet now JSON-encodes list/dict values (str(list)previously brokeunion_base_tables— andsource_tables— on re-import). F009-DERIV-02 losslessness test extended: a union_branches spec's CTE plan is byte-identical across an Excel round-trip.umf.schema.jsonsynced for exactly the fields this PR consumes (the 8 UMFMetadata fields incl. the new enum,union_value,ForeignKey.join_filter,alternative_joins); the full drift regeneration remains AR-2026-03-16's follow-up.docs/guide/sql-plans.md(strategies, filters, dedup, join controls, error philosophy), linked from happy-path §5;excel.mdUnion Value row;docs/api/generators.mdgainsgenerate_sql_plan/SQLPlanGenerator.Deliberately NOT ported
_rewrite_join_filterhardcoded client-specific rewrites (client_mbr_id → ClientMemberId)ValueErrors)merge_strategy,update_mode, pre/post-upsert rules,effective_primary_keyAcceptance shape
The unit suite pins the real downstream cutover this port unblocks (synaptiq-northstar-idr's
silver_fact_inventory_line): two feeds unioned at aDATE '2026-07-20'cutover via complementaryrow_filters, asource_generationdiscriminator viaunion_value, one-sided columns NULL-cast, per-branchROW_NUMBER PARTITION BY arbit_id, cpt, dos, snapshot_date ORDER BY meta_load_dt DESC— compared via sqlglot normalization, not exact text. The conformance twin (gold_union_branches) executes the same shape end-to-end with a portable DATE filter and matches the committed Spark-oracle golden on both DuckDB and Spark (+ pairwise agreement).Testing
tests/unit/test_sql_plan_consumers.py) covering every field's consumer wiring, everyValueErrorpath, CTE-mode single-statement + both-dialect parseability, and the acceptance fixture; model Literal tests; 2 new Excel round-trip testsgold_union_branches(corpus + Spark-generated golden), green on both engine legsfilterwarnings = error; conformance 226 passed; zero existing goldens changed (everything is opt-in); ruff clean; pyright 0 errors on touched modules🤖 Generated with Claude Code