Skip to content

feat(codegen): struct/list containers — TASK-29 Phase B (Tasks 1–2 of 4) - #4

Merged
ahrzb merged 7 commits into
masterfrom
claude/ritchie-dev-08c37b
Jul 23, 2026
Merged

feat(codegen): struct/list containers — TASK-29 Phase B (Tasks 1–2 of 4)#4
ahrzb merged 7 commits into
masterfrom
claude/ritchie-dev-08c37b

Conversation

@ahrzb

@ahrzb ahrzb commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

Teaches the codegen engine to handle SQL struct/list containers as output and as constructed values — retiring 5 of the container-related differential skips (14 → 9). This is the reviewed, shippable first half of Phase B; field access and comparison (Tasks 3–4) are not in this PR.

Plan: docs/superpowers/plans/2026-07-22-codegen-deferred-phase-b-containers.md

What's in this PR

Commit What
88e488c Phase B plan doc
a29b194 Task 1 — struct/list-typed column passthrough: drop the container-output raise in CodegenFn.__init__ so a struct/list column flows through codegen (output model = pydantic submodel / list[...]; _to_native unwraps to dict/list)
32e2df7 Task 1 review fixes (unused import, test reformat, _to_native docstring)
62b890f Task 2 — struct/list construction: new IR nodes StructExpr/ListExpr threaded through _convert_expr → infer_type → _validate_expr → _emit_expr, covering named_struct(...), struct(...), array(...)/make_array(...)
0558fc9 Task 2 review fixes: widen mixed-numeric list elements (int+float → float, matches the DataFusion oracle); narrow the unknown-func defer to the __tfm_ prefix so genuinely unknown funcs hard-error
b03887e Review cleanup — drop dead struct/array branches in the exp.Anonymous arm (verified via sqlglot.parse_one that those route through exp.Struct/exp.Array earlier; only make_array reaches Anonymous)

Files touched

  • sql_transform/_codegen/plan.pyStructExpr/ListExpr IR nodes + convert/infer/validate arms; trimmed _DEFERRED_FUNCS
  • sql_transform/_codegen/engine.py — remove container-output raise; _emit_expr construction arms
  • tests/test_codegen_coverage.py — container shapes moved _DEFERRED → _COMMITTED
  • docs/superpowers/plans/2026-07-22-codegen-deferred-phase-b-containers.md — the plan

Scope limited to sql_transform/_codegen/ and tests/no native-engine changes. DataFusion is the oracle; struct values are Python dicts with significant key order; scalar equality inside containers is type-tagged.

Status — Phase B is half done

Task Shape State
1 struct/list column passthrough ✅ this PR
2 construction (named_struct/struct/array) ✅ this PR
3 struct field access (s.x, nested) ⬜ not started
4 struct/list comparison + container-operand guard ⬜ not started

Tasks 3–4 close the remaining container skips; the final 5 skips are UNNEST, deliberately left for Phase C.

Review & tests

  • requesting-code-review on Tasks 1–2 → ready to merge, no Critical/Important issues; the one Minor finding (dead branches) is fixed in b03887e.
  • uv run pytest512 passed, 9 skipped, 1 xfailed (skip count down from 14).
  • Rebased on current master; test CI check green. (The review check is the anthropics/claude-code-action auto-reviewer failing on its own infra, unrelated to this diff.)

🤖 Generated with Claude Code

@ahrzb
ahrzb force-pushed the claude/ritchie-dev-08c37b branch from 34616f6 to 9f52e9e Compare July 23, 2026 01:00
@claude-agent-ahrzb
claude-agent-ahrzb Bot force-pushed the claude/ritchie-dev-08c37b branch from 9f52e9e to b03887e Compare July 23, 2026 03:01
@ahrzb ahrzb changed the title feat(codegen): struct/list containers — TASK-29 Phase B (Tasks 1–2) feat(codegen): struct/list containers — TASK-29 Phase B (Tasks 1–2 of 4) Jul 23, 2026
@claude-agent-ahrzb
claude-agent-ahrzb Bot force-pushed the claude/ritchie-dev-08c37b branch from b03887e to 7410b90 Compare July 23, 2026 03:50
@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @claude-agent-ahrzb[bot]'s task in 13m 10s —— View job


Review complete

  • Gather context (PR description, diff vs origin/master, repo CLAUDE.md — none found)
  • Understand the request (review Tasks 1–2 of Phase B: struct/list codegen)
  • Review diff for correctness, architecture, tests
  • Post inline comments for specific issues
  • Post final summary review

Summary: No Critical issues. One Important finding: make_array(...), bare positional struct(a, b), and aliased STRUCT(x AS name, ...) construction shapes have zero test coverage (only named_struct and array(x, y)/bracket-list are actually tested for value correctness) — recommend adding coverage before merge. A couple of Minor stale-comment findings also called out inline. Architecture (new StructExpr/ListExpr IR nodes, the mixed-numeric widening fix, the __tfm_-prefix narrowing) is sound and correctly scoped to sql_transform/_codegen/ and tests/.

Full review posted as a top-level PR comment plus 2 inline comments on sql_transform/_codegen/plan.py. I was unable to run uv run pytest in this sandboxed environment (no python execution permission), so this is a static review — please confirm CI is green as well.

Verdict: Ready to merge with fixes.

Comment on lines +396 to +399
# struct(...) parses as exp.Struct and array(...) as exp.Array (handled
# above); only make_array reaches the Anonymous arm.
if name == "make_array":
return ListExpr([_convert_expr(a) for a in e.expressions])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important — test gap: make_array(...) (this branch), bare positional struct(a, b), and the aliased STRUCT(x AS name, ...) form (both handled by _convert_struct below, lines 422-426) have no test coverage anywhere in the diff. I checked tests/test_codegen_coverage.py's _COMMITTED list and tests/test_diff_types.py: only named_struct(...) and array(x, y)/bracket-list [x, y] are exercised for value correctness. A latent bug in the make_array dispatch or in the PropertyEQ/Alias branches of _convert_struct would go undetected.

Recommend adding a _COMMITTED coverage-guard entry plus a differential.check() value-assertion test for each of these three shapes before merging, matching the plan's own "no test = not done" bar.

Comment thread sql_transform/_codegen/plan.py Outdated
Comment on lines +361 to +363
if isinstance(e, exp.Struct):
# sqlglot parses named_struct('k', v, ...) and struct(a, b, ...) authored
# via STRUCT(...) into exp.Struct with exp.PropertyEQ / bare exprs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor — stale comment: this says named_struct(...) parses into exp.Struct, but the code below (lines 394-395) still separately dispatches named_struct from the exp.Anonymous arm via _named_struct, and test_build_plan_constructs_struct_and_list (tests/test_codegen_coverage.py around line 245-247) confirms that's the path actually taken — it expects StructExpr([("k", ...)]) with the literal key name, not the c0/c1 positional-fallback shape _convert_struct would produce. Worth correcting so a future maintainer doesn't mistake the named_struct branch in the Anonymous arm for dead code (the way struct/array were correctly removed as dead code in this PR's last commit).

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review: Tasks 1–2 of Phase B (struct/list codegen)

Reviewed origin/master...HEAD (44ccc9c..7410b90), cross-checked against the plan doc and by tracing _convert_expr → infer_type → _validate_expr → _emit_expr for StructExpr/ListExpr, and confirmed test coverage claims by reading tests/test_codegen_coverage.py, tests/test_diff_types.py, and tests/test_diff_errors.py directly.

Strengths

  • Task 1 (passthrough) is a minimal, correct deletion of the container-output raise, relying on existing field_type_to_python/_to_native/compatible machinery. Covered by a real value-asserting test.
  • Task 2's StructExpr/ListExpr follow the same visitor-dispatch idiom as every other IR node — no bolted-on special-casing.
  • The mixed-numeric widening fix (3a34e1b) correctly reuses _common_base (same helper COALESCE uses) and is backed by a real differential test with an honest, well-documented xfail_on_native for the separate native-side bug.
  • The __tfm_-prefix narrowing closes a real hole (previously any unknown function silently deferred instead of hard-erroring) and has a dedicated regression test.
  • Scope is respected — no changes outside sql_transform/_codegen/, tests/, and the new plan doc.
  • No Critical issues found; I could not execute uv run pytest in this environment (sandboxed, no python execution permission), so please confirm CI is green as the source of truth on top of this static review.

Important (should fix before merge)

  • sql_transform/_codegen/plan.py:396-399 and :418-429make_array(...), bare positional struct(a, b), and aliased STRUCT(x AS name, ...) have zero test coverage. Checked tests/test_codegen_coverage.py's _COMMITTED list and tests/test_diff_types.py: only named_struct(...) and array(x, y)/bracket [x, y] are exercised for value correctness. A latent bug in the make_array dispatch or the PropertyEQ/Alias branches of _convert_struct would go undetected. Left an inline comment with the specific location.

Minor (nice to have)

  • sql_transform/_codegen/plan.py:361-363 — the comment claims named_struct(...) parses into exp.Struct, but it's still separately dispatched from the exp.Anonymous arm via _named_struct (lines 394-395), confirmed by test_build_plan_constructs_struct_and_list. Stale comment risks a future maintainer treating that branch as dead code. Left an inline comment.
  • tests/test_transformer_case.py:60-61 (not part of this diff, so no inline comment could be placed) — the comment "codegen ... already defers named_struct" is now inaccurate; named_struct is fully supported as of this PR. The test still passes, but now defers because of the untranslated __tfm_0__(...) call, not named_struct. Worth a one-line fix for accuracy.
  • No edge-case tests for empty struct()/array()/named_struct(), nested construction, or a NULL element inside a constructed list. The recursive design looks correct by inspection, but these are cheap to add.

Assessment: With fixes. Architecture and the reviewed logic are sound and correctly scoped; add tests for the three untested construction surfaces (and optionally the two stale-comment fixes) before merging.

View job run

ahrzb and others added 6 commits July 23, 2026 06:20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntainer output (TASK-29B-1)

Move struct and list column projection from deferred (UnsupportedInCodegen) to committed
surface. The output model + marshalling infrastructure already handles struct/list
Pydantic models. This unlocks struct/list passthrough for codegen, matching native
engine parity. DataFusion oracle validates.

Changes:
- sql_transform/_codegen/engine.py: Remove the guard block rejecting struct/list containers
- tests/test_codegen_coverage.py: Move struct/list passthroughs from _DEFERRED to _COMMITTED
- sql_transform/_codegen/engine_test.py: Update test to verify struct columns now pass through

Test results: 495 passed (+3), 11 skipped (-3)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…ocstring (TASK-29B-1 review)

- Remove unused UnsupportedInCodegen import from engine_test.py (F401)
- Fix stray blank lines in engine_test.py via ruff format
- Update _to_native docstring: struct columns now reach projected output via passthrough,
  not only via JOIN keys; removed false claim about UnsupportedInCodegen rejection

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… TASK-29 Phase B

Adds StructExpr/ListExpr IR: named_struct(...), struct(...)/STRUCT(...), and
array(...)/make_array(...)/[...] now build a Python dict/list at runtime
instead of raising UnsupportedInCodegen. _DEFERRED_FUNCS shrinks to
("unnest",).

Also tightens the Func emit path in engine.py: an unrecognized function name
(notably a transformer call, __tfm_N__) now raises UnsupportedInCodegen
instead of a generic ValueError -- this path was previously unreachable
because named_struct short-circuited before descending into its arguments;
completing named_struct exposed it. Updated plan_test.py's stale
"named_struct defers" assertion accordingly and added a focused unit test for
the new construction IR.
… defer to __tfm_ (TASK-29B-2 review)

- infer_type's ListExpr arm now unifies element types via _common_base
  (same helper COALESCE uses), so make_array(int, float) types as
  list[float] and pydantic widens the int element at model_validate
  (measured: no emission-side change needed).
- _emit_expr's Func branch narrows the UnsupportedInCodegen defer to
  __tfm_ transformer placeholders only; every other unknown function
  name (e.g. foobar(x), invalid SQL) keeps the original hard ValueError
  instead of being silently skipped by the differential harness.
…B review)

struct(...) parses as exp.Struct and array(...) as exp.Array, both handled
by earlier _convert_expr arms; only make_array reaches the Anonymous arm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude-agent-ahrzb
claude-agent-ahrzb Bot force-pushed the claude/ritchie-dev-08c37b branch from 7410b90 to 76d14f4 Compare July 23, 2026 04:20
…comments (PR#4 review)

Auto-review flagged three committed codegen construction shapes with no value
test: positional struct(a,b) -> c0/c1, named struct(a AS x,...) (exp.PropertyEQ),
and make_array(...). All three match the DataFusion oracle on codegen but native
has no dispatch for them, so they xfail_on_native -- same parity-gap pattern as
the mixed-numeric widening test. Native gaps want their own tickets.

Also fixed two stale comments: plan.py claimed named_struct parses to exp.Struct
(it's exp.Anonymous), and test_transformer_case claimed codegen defers on
named_struct (it defers on the __tfm_0__ transformer call; named_struct is
supported now).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ahrzb

ahrzb commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Addressed in 9d7c0df:

Important (test gap): Added value-asserting differential tests for all three untested construction shapes — positional struct(a, b)c0/c1, named struct(a AS x, ...) (parses as exp.PropertyEQ), and make_array(...). Verified empirically: all three match the DataFusion oracle on codegen, but native has no dispatch for any of them (struct(...) construction and make_array both raise "Unsupported"/"Unknown function"). So a plain check() fails on native — the tests use xfail_on_native, the same parity-gap pattern the mixed-numeric-widening test already established. Codegen correctness is now locked in; the native gaps are flagged for their own tickets.

Minor (stale comments): Both fixed. plan.py now correctly states named_struct parses as exp.Anonymous (not exp.Struct); test_transformer_case.py now says the construct defers on the __tfm_0__ transformer call, not named_struct.

Re the _convert_struct Alias branch: probed the syntaxes — AS and := both yield exp.PropertyEQ, so no construction form produces exp.Alias. Left it as a harmless fallback rather than deleting code I can't confidently prove dead across dialects.

Suite: 515 passed, 9 skipped, 4 xfailed. Not adding empty/nested/NULL-element edge tests unless wanted — the recursive path is exercised by the existing cases.

@ahrzb
ahrzb merged commit d8e56e9 into master Jul 23, 2026
1 check passed
@ahrzb
ahrzb deleted the claude/ritchie-dev-08c37b branch July 23, 2026 16:33
claude-agent-ahrzb Bot pushed a commit that referenced this pull request Jul 26, 2026
 (TASK-45 stretch 4)

Counting-allocator tests now pin zero heap allocation per warm run for
the interpreter AND cranelift, over the probe/arith fixture and a
string-heavy program (case, trim, substr, concat, itos/ftos, parse,
compare). What had to change: substr and trim become pure sub-span
arithmetic (byte ranges into the input span — no copy at all), case
mapping streams char-at-a-time into the arena (Arena::case_map),
number->text formats straight into the arena (Arena::push_fmt) with
DuckF64 rendering to a stack buffer, string compare/parse read the
arena immutably, and h_probe emits values without the per-call
Vec + ScalarVal clones. All shared between backends; 500-seed
differential and the full 612-test pytest suite stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude-agent-ahrzb Bot pushed a commit that referenced this pull request Jul 26, 2026
 (TASK-45 stretch 4)

Counting-allocator tests now pin zero heap allocation per warm run for
the interpreter AND cranelift, over the probe/arith fixture and a
string-heavy program (case, trim, substr, concat, itos/ftos, parse,
compare). What had to change: substr and trim become pure sub-span
arithmetic (byte ranges into the input span — no copy at all), case
mapping streams char-at-a-time into the arena (Arena::case_map),
number->text formats straight into the arena (Arena::push_fmt) with
DuckF64 rendering to a stack buffer, string compare/parse read the
arena immutably, and h_probe emits values without the per-call
Vec + ScalarVal clones. All shared between backends; 500-seed
differential and the full 612-test pytest suite stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant