Skip to content

Match fields by position rather than field number when deep-copying messages - #4631

Merged
g31pranjal merged 8 commits into
mainfrom
apple/g31pranjal/unnested/preserve-record-field-indexes
Nov 2, 2026
Merged

g31pranjal merged 8 commits into
mainfrom
apple/g31pranjal/unnested/preserve-record-field-indexes

Conversation

@g31pranjal

@g31pranjal g31pranjal commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

Makes MessageHelpers.deepCopyMessage() match the fields of the source and target descriptors by position rather than by field number.

Problem

A record type read off a descriptor keeps the descriptor's field numbers, which may skip, while a type built up field by field (e.g. by Type.maximumType() for the element type of a multi-row VALUES list) is numbered by position. Two descriptors of the same structure can therefore number the same field differently, and a copy that matches fields by number writes a value into the wrong field. For a table whose proto is numbered first_name=1, <2 skipped>, last_name=3, id=4, last_name ends up in id:

IllegalArgumentException: Wrong object type used with protocol message reflection.
Field number: 3, field java type: LONG, value type: java.lang.String

Changes

  • deepCopyMessage() matches fields by position. The target descriptor may have additional trailing fields, such as the pseudo-fields of a record type.
  • Tests in MessageHelperTests cover nested records, enums, repeated and nullable repeated records, and a target with pseudo-fields.

The defect is latent on main. It shows up once every explode wraps its elements in a record constructor, as in #4816, where skipped-field-number-proto.yamsql and field-index-tests-proto.yamsql fail without this fix.

@g31pranjal g31pranjal added the bug fix Change that fixes a bug label Sep 17, 2026
@g31pranjal
g31pranjal added this pull request to stack #4632 September 17, 2026 22:35
@g31pranjal
g31pranjal removed this pull request from stack #4632 September 17, 2026 22:39
@g31pranjal
g31pranjal force-pushed the apple/g31pranjal/unnested/explode-ordinality-matching branch from 2979d09 to c781f27 Compare September 17, 2026 22:39
@g31pranjal
g31pranjal force-pushed the apple/g31pranjal/unnested/preserve-record-field-indexes branch from 2fe1f19 to 10e1538 Compare September 17, 2026 22:39
@g31pranjal
g31pranjal added this pull request to stack #4634 September 17, 2026 22:44
@g31pranjal
g31pranjal removed this pull request from stack #4634 September 17, 2026 22:48
@g31pranjal
g31pranjal changed the base branch from apple/g31pranjal/unnested/explode-ordinality-matching to main September 18, 2026 10:58
@g31pranjal
g31pranjal force-pushed the apple/g31pranjal/unnested/preserve-record-field-indexes branch from 10e1538 to 800e6e0 Compare September 18, 2026 10:59
@g31pranjal
g31pranjal marked this pull request as ready for review September 18, 2026 11:48
@g31pranjal
g31pranjal force-pushed the apple/g31pranjal/unnested/preserve-record-field-indexes branch from ea52626 to 7f4ede7 Compare October 4, 2026 21:38
…ximum

A record type read off a descriptor carries the descriptor's field numbers, and
those numbers may skip. `defineProtoType()` writes them back out as the field
numbers of the descriptor it generates for the type, so two types that disagree
about them generate descriptors that are not wire-compatible with each other.

`maximumType()` rebuilt each field with `Field.of(type, name)`, which has no
index, so `normalizeFields()` renumbered every field by position. The maximum of
two record types that agree on their numbering therefore lost that numbering, and
`MessageHelpers.deepCopyMessage()`, which relates the fields of two descriptors by
number, would then route a value into the field that happens to carry the same
number in the renumbered descriptor -- writing, for instance, a string into a
field declared `int64`.

Carry the index over whenever both sides agree on it, by the same rule already
used for the field name. The indexes are only kept when all of them survive, since
`normalizeFields()` renumbers all fields as soon as a single one is missing.
…ld number

A record type read off a descriptor carries the descriptor's field numbers, and
those numbers may skip. Two descriptors generated for record types that agree on
everything but the field numbers therefore number the same field differently.
`MessageHelpers.deepCopyMessage()` relates the fields of two descriptors by number,
so copying a message between such descriptors puts a value into whichever field
happens to carry its number on the other side -- writing, for instance, a string
into a field declared `int64`.

Instead of keeping the field numbers alive through `Type.maximumType()`, coerce by
position: `coerceObject()` without any promotions now copies a message through the
new `coerceMessageByIndexIfNeeded()`, which relates the fields of the two
descriptors by their position, just as `coerceMessage()` already does when there
are promotions to apply.
@g31pranjal
g31pranjal force-pushed the apple/g31pranjal/unnested/preserve-record-field-indexes branch from 7f4ede7 to 6f6111a Compare November 2, 2026 00:32
@g31pranjal g31pranjal changed the title Keep the field indexes two record types agree on when taking their maximum Coerce messages between descriptors by field position rather than field number Nov 2, 2026
…essages

Two descriptors of the same structure need not number its fields the same way,
e.g. if one of them keeps the numbers of a proto that skips some while the other
is numbered by position. `MessageHelpers.deepCopyMessage()` matched the fields of
the two descriptors by number, so a copy between such descriptors put a value into
whichever field happened to carry its number on the other side -- writing, for
instance, a string into a field declared `int64`.

Match the fields by position instead, which leaves the copy unchanged wherever the
numbering already agrees. The target descriptor may have additional trailing fields,
such as the pseudo-fields of a record type. A nested message is now also copied into
the target's message type rather than set as is.

This replaces the earlier approach of keeping the field indexes alive through
`Type.maximumType()` and of coercing by position in `coerceObject()`.
@g31pranjal g31pranjal changed the title Coerce messages between descriptors by field position rather than field number Match fields by position rather than field number when deep-copying messages Nov 2, 2026
The branch that sets a nested message as is never runs for the DynamicMessage
builders deepCopyMessage is used with, as their default instances are never
identical, so the merge branch already copies nested messages into the target's
message type.
// find the field on the target side
final var targetField = builder.getDescriptorForType().findFieldByNumber(field.getNumber());
// find the field on the target side, by position
final var targetFields = builder.getDescriptorForType().getFields();

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.

can you move this out of the loop

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

done!

@g31pranjal
g31pranjal requested a review from normen662 November 2, 2026 15:18
@g31pranjal
g31pranjal force-pushed the apple/g31pranjal/unnested/preserve-record-field-indexes branch from 8876285 to f41988e Compare November 2, 2026 16:14
@g31pranjal
g31pranjal merged commit 3f5ef5d into main Nov 2, 2026
14 checks passed
@g31pranjal
g31pranjal deleted the apple/g31pranjal/unnested/preserve-record-field-indexes branch November 2, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug fix Change that fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants