Skip to content

Reconcile CLUSTER/MERGE star-passthrough column resolution with the pass-1 resolver's reference model #176

Description

@conradbzura

Context

Surfaced during the round-1 review of PR #175 (Closes #164) as advisory findings A4 and A5 (plus the A11(b) note). Both concern reconciling the CLUSTER/MERGE star-passthrough column-resolution toolkit (src/giql/expanders/_genomic.py) with the pass-1 resolver (src/giql/expanders/resolver.py). This is a non-blocking refactor / correctness-hardening follow-up — PR #175 shipped with a documentation-only note per the reviewers' recommended remediations; the actual reconciliation is deferred here.

A4 — Two conflicting models of "what columns does a subquery expose", and the latent bug it hides

For the same input (SELECT * FROM regions) with a custom-mapped regions, the two subsystems disagree:

  • _genomic._resolve_query traces a SELECT * / alias.* FROM source through to the underlying table's physical genomic columns, honoring custom mappings. This is the more correct model.
  • resolver.py's reference-slot machinery_resolve_disjoin_reference (~731-799) and validate_projection_contracts (~1175-1344) — assumes canonical columns for a subquery/CTE reference operand, and even passes a SELECT * through as canonical.

CLUSTER/MERGE are deliberately carved out of the reference-slot framework, so the divergence is defensible for those operators — but the resolver's assume-canonical model is a latent bug for the operators that do use the reference path: DISJOIN and NEAREST currently assume canonical columns and would emit phantom columns (chrom/start/end) for a SELECT *-over-a-custom-mapped-table reference, rather than the table's actual physical columns.

Follow-up: Teach the resolver's reference path to trace star-passthrough sources the same way _genomic._resolve_query does. This reconciles the two models onto the more-correct (trace-through) one and fixes the latent DISJOIN/NEAREST SELECT *-over-custom-mapped-table bug.

A5 — The toolkit hand-rolls source/CTE/mapping logic the resolver already owns

_genomic.py re-implements machinery the pass-1 resolver already provides, inviting drift:

  • _find_cte re-introduces a hand-rolled .parent / with_ ancestor walk that resolver.py deliberately replaced with scope.cte_sources.
  • _mapping_for duplicates resolver._physical_cols (~709-728).
  • The star / canonical-subset check in _resolve_query mirrors resolver._static_output_columns (~1280-1308), with inverted polarity.

Follow-up: Extract shared leaf helpers — a common physical_columns(table) and static_output_columns() — that both subsystems consume, so the two do not drift apart.

Related (A11(b)): _genomic._CANONICAL_COLUMNS is a third representation of the canonical-default columns, alongside resolver._DEFAULT_COLS and resolver._physical_cols(None). It is a candidate to unify onto the shared helper if the extraction above lands.

References

https://claude.ai/code/session_01KAYsMtN7vYuECZHeeFFzCM

Metadata

Metadata

Assignees

No one assigned

    Labels

    refactorCode restructuring without behavior change

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions