Skip to content

fix: extension-type metadata dropped for list-backed logical types (ITL-627) - #257

Merged
brian-arnold merged 13 commits into
mainfrom
eywalker/itl-627-extension-type-metadata-dropped-for-list-backed-logical
Aug 26, 2026
Merged

fix: extension-type metadata dropped for list-backed logical types (ITL-627)#257
brian-arnold merged 13 commits into
mainfrom
eywalker/itl-627-extension-type-metadata-dropped-for-list-backed-logical

Conversation

@kurodo3

@kurodo3 kurodo3 Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes three defects where ListLogicalType-backed (list[T] / set[T]) Arrow extension columns were mishandled downstream:

  • Defect 1 (Polars round-trip raises): ListLogicalType.get_polars_extension_type() was not passing metadata= to make_polars_extension_type, so ext_metadata() returned None. Polars exported b'' on to_arrow(), which _deserialize rejected with ValueError during Join and MergeJoin round-trips.
  • Defect 2 (Content hashing bypassed): SemanticHashingVisitor.visit_extension short-circuited on not isinstance(python_type, type) for list[File] (a types.GenericAlias), so raw JSON path strings were hashed instead of file contents.
  • Defect 3 (MergeJoin loses extension wrapper): MergeJoin.binary_static_process used pa.array(merged_vals) which inferred type from raw storage, producing large_list(storage_type) instead of extension<list[orcapod.file]> for merged logical-type columns.

Changes

File Change
src/orcapod/logical_types/list_logical_type_factory.py Fix 1: pass metadata=self._metadata_bytes.decode("utf-8") to make_polars_extension_type
src/orcapod/hashing/visitors.py Fix 2: detect list/set-backed extension types in visit_extension before the isinstance guard; delegate to _visit_list_elements; handles set[T], list[list[T]], and arbitrary nesting
src/orcapod/core/operators/merge_join.py Fix 3: snapshot colliding column Arrow types before Polars round-trip; build merged arrays via pa.ExtensionArray.from_storage(ListLogicalType, ...)
tests/test_logical_types/test_list_logical_type.py New: 3 unit regression tests for Fix 1
tests/test_core/operators/test_operators.py New: TestJoinWithListExtensionColumn
tests/test_core/operators/test_merge_join.py New: TestMergeJoinWithListExtensionColumn + TestMergeJoinLogicalTypeColumns (Fix 3 scalar + nested cases)
tests/test_hashing/test_extension_type_hashing.py New: TestListExtensionHashing (9 tests: list[File], set[File], list[list[File]], struct field, passthrough, content-change, determinism, symmetry)

Test plan

  • All 4709 existing tests pass (uv run pytest tests/)
  • uv run pytest tests/test_logical_types/ — Fix 1 unit tests
  • uv run pytest tests/test_hashing/test_extension_type_hashing.py::TestListExtensionHashing — Fix 2 regression tests
  • uv run pytest tests/test_core/operators/test_merge_join.py::TestMergeJoinLogicalTypeColumns — Fix 3 regression tests
  • uv run pytest tests/test_core/operators/ — full operators suite including test-ordering safety

Fixes ITL-627

🤖 Generated with Claude Code

kurodo3 Bot and others added 11 commits August 22, 2026 00:39
…ixes

Root causes identified:
- ListLogicalType.get_polars_extension_type() omits metadata= arg, causing
  Polars to export b'' on to_arrow() which _deserialize rejects
- SemanticHashingVisitor.visit_extension short-circuits on isinstance check
  for generic aliases like list[File]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Defect 3: MergeJoin drops extension type when aggregating logical-type columns
- Expand Defect 2 tests: set[File], list[list[T]], Dataclass with list[T] field
- Add list[File] x list[File] -> list[list[File]] MergeJoin case (was wrongly out of scope)
- Correct Fix 3: binary_output_schema needs no change (Schema stores Python types already correct)
- Add implementation plan with complete code for all 4 tasks
…stLogicalType

ListLogicalType.get_polars_extension_type() was not passing the JSON metadata
bytes to make_polars_extension_type, so ext_metadata() returned None. Polars
exported b'' on to_arrow(), which _deserialize rejected with ValueError during
the Join/MergeJoin Polars round-trip. One-line fix covers both list[T] and set[T].

Fixes ITL-627 (Defect 1).
…-trip

Exercises the full Join and MergeJoin Polars round-trip with list[Path] extension
columns, confirming Fix 1 (ITL-627 Defect 1) at the operator integration level.
…ion boilerplate

Keep registration boilerplate in both join tests and add a comment explaining
why it is required: ArrowTableStream does not trigger LogicalType registration,
so without explicit pa/pl registration Polars degrades the extension type to
its storage type during the operator's round-trip. Add row-count and value
spot-check assertions to verify data integrity after the join.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SemanticHashingVisitor.visit_extension previously short-circuited on
`not isinstance(python_type, type)` for list[File] (a GenericAlias),
returning the extension type unchanged — raw JSON path strings were
hashed instead of file contents.

Fix: detect list-backed extension types before the isinstance guard and
delegate to _visit_list_elements with a virtual large_list(elem_ext_type)
so each element is hashed identically to the scalar visit_extension path.
Handles set[T], list[list[T]], and arbitrary nesting depth via recursion.

Adds 9 regression tests covering: list[File], set[File], list[list[File]],
struct with list[File] field, passthrough when no handler, passthrough for
list[Path], content-change sensitivity, same-content determinism, and
element-wise hash symmetry with scalar File hashing.
…docstring

- Remove redundant from-body imports of File, pa, uuid, PythonTypeHandlerRegistry,
  SemanticAwarePythonHasher in test_extension_type_hashing.py — all were already
  available at module level.
- Add LogicalPath, ListLogicalType, PythonTypeHandlerRegistry, SemanticAwarePythonHasher
  to module-level imports.
- Add idempotency note to _make_list_file_ext_type / _make_scalar_file_ext_type helpers.
- Add Args:/Returns: sections and passthrough-case documentation to
  SemanticHashingVisitor.visit_extension docstring (Google style).
- Add defensive-guard comment on the `if args:` fallthrough.
…logical-type columns

binary_static_process used pa.array(merged_vals) which inferred array type from
raw storage values, producing plain large_list(storage_type) and losing the
extension wrapper. Fix snapshots the element Arrow type before the Polars round-trip,
then builds the merged array as pa.ExtensionArray.from_storage(ListLogicalType, ...)
when the element is an extension type. Handles nested list[list[T]] naturally.

Fixes ITL-627 (Defect 3).
…s; improve test messages

- Rename list_lt to list_logical_type in binary_static_process for clarity.
- Add late-import comment explaining the circular dependency guard.
- Add failure messages to shape assertions in TestMergeJoinLogicalTypeColumns.
… Join/MergeJoin regression tests

Fresh ListLogicalType instances create different underlying Arrow extension classes.
When two tests both call pa.register_extension_type() for the same extension name,
the second call is a no-op — so the second test holds a class object that is never
globally registered. Join's table.cast() then tries to cast between two different
class objects for the same extension name, raising ArrowTypeError.

Fix: use ctx.type_converter.register_python_class(list[Path]) which goes through
the type converter's shared cache, guaranteeing the same class object across tests.
@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/orcapod/core/operators/merge_join.py 91.66% 1 Missing ⚠️
src/orcapod/hashing/visitors.py 95.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes ITL-627 by ensuring ListLogicalType-backed Arrow extension columns retain their extension metadata through Polars round-trips, participate correctly in semantic content hashing, and remain extension-typed when merged by MergeJoin.

Changes:

  • Preserve Polars extension metadata for list[T] / set[T] by passing JSON metadata= when constructing the Polars extension type.
  • Teach SemanticHashingVisitor.visit_extension to semantically hash list/set-backed extension columns element-by-element (including nested list/set cases) instead of incorrectly passing them through.
  • Rebuild merged colliding columns in MergeJoin using the appropriate list extension type (instead of type inference from raw storage), with regression tests covering scalar and nested list logical types.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/orcapod/logical_types/list_logical_type_factory.py Passes JSON metadata into the Polars extension type so Polars→Arrow round-trips don’t drop/empty extension metadata.
src/orcapod/hashing/visitors.py Fixes semantic hashing for list/set-backed extension types by routing through list element visitation (supports nesting).
src/orcapod/core/operators/merge_join.py Preserves extension wrappers when merging colliding logical-type columns by reconstructing via the list logical type’s Arrow extension type.
tests/test_logical_types/test_list_logical_type.py Adds targeted regression tests validating Polars extension metadata is present and survives Polars→Arrow.
tests/test_hashing/test_extension_type_hashing.py Adds comprehensive regression/contract tests for semantic hashing of list[File] / set[File] and nested forms.
tests/test_core/operators/test_operators.py Adds Join integration regression test for list extension columns surviving the Polars round-trip.
tests/test_core/operators/test_merge_join.py Adds MergeJoin integration/regression tests for non-colliding and colliding logical-type list columns (scalar + nested).
superpowers/specs/2026-08-22-itl-627-list-extension-metadata.md Captures problem statement, root cause analysis, and intended fixes/tests for ITL-627.
superpowers/plans/2026-08-22-itl-627-list-extension-metadata.md Implementation plan detailing steps, file map, and test strategy for the three defects.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@brian-arnold brian-arnold left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at 8a9f4a33; verified each point empirically in a worktree against both this branch and main. Four inline comments: two crashes in the new list-backed hashing path (one a regression from main), a silent list[T]/set[T] hash collision, an order-dependent test failure that the full-suite run masks, and a context-plumbing nit in MergeJoin.

Two things worth adding to the PR description: the hashing change invalidates content hashes for every list-backed extension column, so existing FunctionNode/OperatorNode records keyed on those hashes become cache misses and get recomputed; and the bundled polling-source _combine change isn't mentioned (it also drops sort_by_tags, so the accumulated stream is no longer tag-sorted per batch). Per CLAUDE.md, the three defects fixed here don't appear in DESIGN_ISSUES.md.

Comment thread src/orcapod/hashing/visitors.py Outdated
elem_python_type
)
virtual_list_type = pa.large_list(elem_arrow_type)
return self._visit_list_elements(virtual_list_type, storage_value)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two crash paths here share one root cause: _visit_list_elements can return a list type whose element type is still a live pa.ExtensionType, and _process_table_columns then calls pa.array(data, type=large_list(extension<...>)), which Arrow rejects. Whole-column passthrough is safe because normalize_extension_columns converts it to storage type + field metadata; an extension sitting in a list value field never gets that treatment.

(a) Nested alias whose innermost type has no handler. is_nested_list_or_set (L255) commits to recursing before knowing whether the inner level can actually hash. For list[list[Path]]: outer recurses, inner visit_extension sees element Path, finds no handler, passes through, and the outer level builds large_list(extension<list[orcapod.path]>). Verified: a one-row extension<list[list[orcapod.path]]> column raises RuntimeError: Failed to process column 'x': extension, while the same table hashes to 000001c6 on main. So this is a regression, not merely an unsupported case.

(b) Empty or all-null inner list. _visit_list_elements falls back to new_element_type = element_type (the element extension type) when no processed element is non-None, and _process_table_columns latches the first row's type for the whole column. Verified on extension<list[orcapod.file]>: [[], [file]], [[None], [file]] and [[]] all raise; [[file], []] is fine. Same for list[uuid.UUID]. This is reachable in normal use — ListLogicalType.python_to_storage(None) returns [], so any row with a None/empty list[File] that happens to sort first breaks hashing for the entire table.

Suggested shape for the fix: make the hash-vs-passthrough decision purely type-driven (so every row of a column takes the same branch) and derive the new list type from that decision rather than from the row data:

# unwrap list/set nesting down to the innermost plain type
inner = elem_python_type
while typing.get_origin(inner) in (list, set):
    inner = typing.get_args(inner)[0]
hashable = isinstance(inner, type) and self._python_hasher.type_handler_registry.has_handler(inner)
if not hashable:
    return extension_type, storage_value  # whole-column passthrough, as on main

and then build the target type from the nesting depth (large_list(large_binary), large_list(large_list(large_binary)), ...) instead of letting _visit_list_elements infer it from the first non-None row. That covers (b) as well without changing the shared _visit_list_elements used by visit_list.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The list-backed detection block now makes a single type-driven hashability decision before touching any rows:

  1. Unwrap the element type through any list/set nesting to reach the innermost plain type.
  2. Check whether that innermost type has a semantic handler.
  3. If not hashable: whole-column passthrough (same as ).
  4. If hashable: call , but discard its returned type (using ). The output type is set to unconditionally, so empty and null inner lists no longer latch an extension type in .

Crash (a) — regression: because Path has no handler, the innermost-type check returns and the entire column passes through before any row is visited.

Crash (b) — empty/null inner list: still falls back to the element extension type when all rows are null/empty (its existing behaviour), but the returned type is discarded. The outer call always returns regardless of data content.

# We intercept here and delegate to _visit_list_elements with a virtual
# large_list(elem_ext_type) so each element goes through visit_extension.
if (
typing.get_origin(python_type) in (list, set)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The outer wrapper's extension identity is dropped on this branch. The scalar path below (L281-289) deliberately folds extension_type.extension_name into the hash token, but this branch returns a bare large_list(large_binary) with no record of which wrapper produced it.

Verified: single-row extension<list[orcapod.file]> and extension<set[orcapod.file]> tables holding the same file both hash to 0000019c292aa9a0 on this branch; on main they differ (000001ea... vs 000001df...). Two structurally distinct logical types are now indistinguishable to hash_table, so a memoized record keyed on one can be served for the other.

This is a silent-wrong-result bug rather than a crash, and the fix is small (include extension_type.extension_name in the result the way the scalar path does). Worth doing before merge, since fixing it later is another cache-invalidating hash change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The result now includes the outer extension name the same way the scalar path does.

The list-backed path now returns (pa.large_binary(), combined_bytes) where combined_bytes = type_name.encode() + b"::" + b"\x00".join(element_hashes) and type_name = extension_type.extension_name.replace(".", ":"). For extension<list[orcapod.file]> this produces b"list[orcapod:file]::<h0>\x00<h1>". For extension<set[orcapod.file]> with the same files it produces b"set[orcapod:file]::<h0>\x00<h1>" — distinct because the outer wrapper name differs.

Added test_list_and_set_file_extension_produce_distinct_hashes to directly assert the two hashes differ.

# Register the extension types with both Arrow and Polars registries so
# the round-trip can reconstruct the extension type (not fall back to storage).
try:
pa.register_extension_type(ext_type)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This test constructs a fresh ListLogicalType(LogicalPath()) (bypassing the converter registry) and registers it globally via pa.register_extension_type / pl.register_extension_type with no teardown. The global PyArrow/Polars registries then hold a different _ArrowExt_list_orcapod_path_ class than the one the orcapod registry hands out.

uv run pytest tests/test_logical_types tests/test_core/operators fails with:

ArrowTypeError: Casting from 'extension<list[orcapod.path]>' to different extension type 'extension<list[orcapod.path]>' not permitted

in TestJoinWithListExtensionColumn::test_join_preserves_list_extension_column — another test added by this PR. The full tests/ run passes (4711) only because test_core sorts before test_logical_types, so the green checklist is an artifact of collection order.

Fix: obtain the type from the registry the way the other new tests do — ctx.type_converter.get_logical_type_for_python_type(list[Path]) — or unregister in a fixture teardown.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The test now uses ctx.type_converter.register_python_class(list[Path]) instead of constructing a fresh ListLogicalType and calling pa.register_extension_type / pl.register_extension_type manually.

register_python_class calls register_logical_type internally, which registers with both the Arrow and Polars global registries using the same class object the orcapod registry holds. The fresh-instance approach registered a different class under the same extension name, causing the ArrowTypeError: Casting from extension<list[orcapod.path]> to different extension type failure when tests ran in test_logical_typestest_core/operators order.

Verified: uv run pytest tests/test_logical_types tests/test_core/operators now passes (698 tests, 0 failures).

# Late import: orcapod.contexts pulls in the full type system;
# importing at module level creates a circular dependency.
from orcapod.contexts import get_default_context
tc = get_default_context().type_converter

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

get_default_context() here bypasses the operator's own context. Every other type-converter call in this layer goes through the input stream's context — stream.data_context.type_converter in batch.py, group_by.py, index.py, pick.py — so this should be left_stream.data_context.type_converter. The "circular dependency" rationale in the comment doesn't apply either: reaching the stream's context needs no import.

Concretely: a MergeJoin over streams built with a non-default DataContext reconstructs the merged column's extension type from the default registry. If the two registries hold distinct classes for the same extension name, you hit exactly the ArrowTypeError: ... different extension type ... not permitted failure described in the test-ordering comment on tests/test_logical_types/test_list_logical_type.py.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Replaced get_default_context().type_converter with left_stream.data_context.type_converter, hoisted above the colliding-keys loop, and removed the late import of get_default_context.

The "circular dependency" comment was wrong — reaching stream.data_context needs no module-level import. The fix matches the pattern used in index.py and pick.py.

@brian-arnold

Copy link
Copy Markdown
Collaborator

Correction to my review summary above: the polling-source _combine point doesn't belong to this PR — #257 touches only the hashing/list-logical-type/merge-join files and their tests, with no polling source change. Please disregard that sentence; the rest of the summary and the four inline comments stand.

1. **Test contamination** (test_list_logical_type.py): Replace the fresh
   `ListLogicalType(LogicalPath())` + manual `pa.register_extension_type` /
   `pl.register_extension_type` with `ctx.type_converter.register_python_class(list[Path])`.
   The manual approach registered a different class object than the orcapod
   registry held, causing `ArrowTypeError: Casting from extension<list[orcapod.path]>
   to different extension type` when tests ran in `test_logical_types` before
   `test_core/operators` order.

2. **list[T] / set[T] hash collision** (visitors.py): The outer extension name was
   not encoded in the result, so `extension<list[orcapod.file]>` and
   `extension<set[orcapod.file]>` with identical contents produced identical hashes.
   Fixed by folding `extension_type.extension_name` into the combined result,
   mirroring the scalar path encoding.

3. **Two crash paths** (visitors.py): (a) `list[list[Path]]` — `is_nested_list_or_set`
   committed to recursing before checking whether the innermost type has a handler,
   causing `_visit_list_elements` to return `large_list(extension<...>)` which Arrow
   rejects in array creation. (b) Empty/null inner list — `_visit_list_elements`
   fell back to the element extension type when no non-null element was present,
   latching an extension type in `_process_table_columns`. Fixed by making the
   hash-vs-passthrough decision type-driven (unwrap nesting to innermost, check
   handler) before visiting any rows, and discarding the returned list type from
   `_visit_list_elements` (using `_`) since we return `(large_binary(), combined)`
   regardless.

4. **Wrong context in MergeJoin** (merge_join.py): `get_default_context().type_converter`
   bypassed the operator's actual context. Replaced with `left_stream.data_context.type_converter`,
   hoisted above the colliding-keys loop, and removed the late import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@brian-arnold

Copy link
Copy Markdown
Collaborator

Tested these changes on my end and they appear to resolve the issues!

@brian-arnold
brian-arnold self-requested a review August 26, 2026 01:02
@kurodo3

kurodo3 Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Response to brian-arnold's review (round 2)

All four issues addressed in commit fb2b0623.

# Issue Fix summary
1 Test contamination (test_list_logical_type.py L356) test_polars_to_arrow_round_trip_preserves_extension_type replaced fresh ListLogicalType(LogicalPath()) + manual pa/pl.register_extension_type with ctx.type_converter.register_python_class(list[Path]), which uses the same class object as the orcapod registry. uv run pytest tests/test_logical_types tests/test_core/operators now passes (698).
2 Silent hash collision (visitors.py L236) List-backed path now returns (pa.large_binary(), type_name_bytes + b"::" + b"\x00".join(elem_hashes)), folding the outer extension name into the result exactly as the scalar path does. list[File] and set[File] with identical contents now produce distinct hashes. Added test_list_and_set_file_extension_produce_distinct_hashes.
3a Crash — nested unhashable alias (visitors.py L255) Detection block now unwraps list/set nesting to the innermost type and checks handler existence before visiting any rows. list[list[Path]] gets hashable = False (Path has no handler) and passes through immediately.
3b Crash — empty/null inner list (visitors.py) _visit_list_elements return type is discarded (bound to _). The outer call always returns (pa.large_binary(), combined_bytes) regardless of data content, so an empty inner list no longer latches an extension type in _process_table_columns.
4 Wrong context in MergeJoin (merge_join.py L290) Replaced get_default_context().type_converter with left_stream.data_context.type_converter, hoisted above the colliding-keys loop, removed the late get_default_context import.

Tests updated in test_extension_type_hashing.py: the five assertions on large_list(large_binary) are updated to large_binary, list-subscript element checks are rewritten to split the combined bytes by \x00, and the new collision-prevention test is added.

@kurodo3

kurodo3 Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Response to brian-arnold review (round 2), commit fb2b062.

Four issues fixed:

  1. Test contamination (test_list_logical_type.py L356): replaced fresh ListLogicalType + manual pa/pl.register_extension_type with ctx.type_converter.register_python_class(list[Path]). This uses the same class object as the orcapod registry. uv run pytest tests/test_logical_types tests/test_core/operators now passes all 698 tests.

  2. Silent hash collision (visitors.py L236): the list-backed path now returns (pa.large_binary(), outer_type_name + '::' + joined_elem_hashes), folding the outer extension name into the result exactly as the scalar path does. list[File] and set[File] with identical contents now produce distinct hashes. Added test_list_and_set_file_extension_produce_distinct_hashes.

3a. Crash — nested unhashable alias (visitors.py L255): the detection block now unwraps list/set nesting to the innermost type and checks handler existence before visiting any rows. list[list[Path]] gets hashable=False (Path has no handler) and passes through immediately, restoring main-branch behaviour.

3b. Crash — empty/null inner list (visitors.py): _visit_list_elements return type is discarded. The outer call always returns (pa.large_binary(), combined_bytes) regardless of data content, so an empty inner list no longer latches an extension type in _process_table_columns.

  1. Wrong context in MergeJoin (merge_join.py L290): replaced get_default_context().type_converter with left_stream.data_context.type_converter, hoisted above the colliding-keys loop, removed the late import.

Tests updated in test_extension_type_hashing.py: five assertions on large_list(large_binary) updated to large_binary, list-subscript element checks rewritten to split combined bytes by \x00, new collision-prevention test added.

@brian-arnold
brian-arnold merged commit 707c1b9 into main Aug 26, 2026
11 checks passed
brian-arnold pushed a commit that referenced this pull request Aug 27, 2026
…204)

ITL-627 Defect 2 reported that list-wrapping a File column lost content
hashing: build_aggregated_table produced a list[orcapod.file] hashed by its
storage values (JSON path strings), so editing a file silently failed to
invalidate. Verified against 8968018, before this branch was rebased.

It does not reproduce on the current base -- #257 fixed it in
SemanticHashingVisitor.visit_extension, which previously short-circuited for
generic aliases like list[File]. That fix added thorough unit coverage in
test_extension_type_hashing.py::TestListExtensionHashing, but nothing exercises
the job level, which is where the failure was observed and where it fails
silently rather than raising.

Adds a parametrized per-row/grouped test: cold run executes, identical re-run
hits the cache, and a content edit at the same path re-executes. Mutation-checked
by disabling the list/set unwrapping branch in visitors.py -- the grouped case
fails while per_row still passes, so the test isolates the list-wrapping
regression rather than scalar File hashing.

NPIPE-204
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.

3 participants