Skip to content

Fix collection partial leaking fields between iterations (#64)#65

Open
jujustayfly wants to merge 1 commit into
mainfrom
fix-collections-leak
Open

Fix collection partial leaking fields between iterations (#64)#65
jujustayfly wants to merge 1 commit into
mainfrom
fix-collections-leak

Conversation

@jujustayfly
Copy link
Copy Markdown

Summary

Fixes #64.

PbbuilderTemplate#_render_collection_with_options allocates one sub-message and reuses it for every iteration of the partial, so fields that aren't actively rewritten leak from the previous element. Two visible shapes:

  • Repeated message blocks accumulate. pb.field collection do |x| ... end inside the partial routes through _append_repeated, which pushes onto the shared message's repeated field, so each iteration sees all prior elements.
  • Conditionally-set fields leak. A partial with an if/elsif that writes a field only on some branches inherits the previous element's value on the silent branch.

Manual iteration (pb.field coll.each do |x| ... end at the top level) is unaffected because _append_repeated already allocates a fresh sub-message per element.

Fix

Reset the working message on the shared pb between iterations inside Pbbuilder::CollectionRenderer#build_rendered_template. After snapshotting the body for the just-rendered element, swap @message for a fresh sub-message of the right type via pb_parent.new_message_for(field). The first iteration is already clean (initialized in _render_collection_with_options), so every iteration now starts from a fresh message.

This brings the partial:/collection: shorthand to parity with the per-element allocation manual iteration already does in _append_repeated — no extra wrapper allocation, just one extra protobuf message per element (same cost as the workaround documented in the issue).

A small _reset_message(message) helper was added to Pbbuilder because the class inherits from BasicObject and doesn't expose instance_variable_set (calling it would route through method_missingset! and raise "Unknown field").

`PbbuilderTemplate#_render_collection_with_options` allocated a single
sub-message and reused it across every iteration of the partial, so any
field not actively rewritten on a given iteration carried over from the
previous one. In particular:

- Repeated message fields written via `pb.field collection do |x| ... end`
  accumulated across iterations because `_append_repeated` pushes onto
  the shared message.
- Conditionally-written fields leaked the previous element's value into
  elements that took the silent branch.

Reset the working message on the shared `pb` between iterations inside
`Pbbuilder::CollectionRenderer#build_rendered_template`, so each render
starts with a clean sub-message of the right type. This brings the
`partial:`/`collection:` shorthand to parity with the per-element
allocation already done by manual iteration through `_append_repeated`.

Adds a `_reset_message` helper on `Pbbuilder` (since the class inherits
from `BasicObject` and doesn't expose `instance_variable_set`) plus two
regression tests covering both leak shapes.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jujustayfly jujustayfly requested a review from tzaid May 7, 2026 14:04
Copy link
Copy Markdown

@tzaid tzaid left a comment

Choose a reason for hiding this comment

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

Good catch!

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.

Collection rendering with partial:/collection: shorthand reuses one message across iterations, leaking unset fields between elements

2 participants