Skip to content

Pin protobuf field and enum value numbers in a sidecar artifact#1304

Open
jwils wants to merge 1 commit into
mainfrom
joshuaw/proto-field-numbers
Open

Pin protobuf field and enum value numbers in a sidecar artifact#1304
jwils wants to merge 1 commit into
mainfrom
joshuaw/proto-field-numbers

Conversation

@jwils

@jwils jwils commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Why

With #1080's sequential numbering, reordering or removing a field renumbers everything after it — breaking wire compatibility with previously serialized data. Protobuf field numbers must stay stable across schema evolution.

What

  • schema_artifacts:dump now reads and writes a proto_field_numbers.yaml sidecar artifact: existing numbers stay fixed even if field order changes, and new fields get the next available numbers
  • schema.proto keeps the public GraphQL field names while the sidecar stores private name_in_index overrides
  • a field renamed with field.renamed_from reuses its existing number under the new public name
  • enum value numbers are pinned in an enums section; removed values keep their numbers reserved so they are never reused (0 remains the generated *_UNSPECIFIED value)
  • mappings are validated: collisions raise clear errors, and non-integer numbers are rejected instead of letting Integer() silently truncate
  • configure_proto_field_number_mappings lets tests and advanced callers seed mappings without an artifact file

Risk Assessment

Low — only affects the unreleased elasticgraph-proto_ingestion extension from #1080.

References


Update — 2026-07-15

  • Merged the latest Generate a protobuf schema artifact from ElasticGraph schemas #1080 generator refactor so reachable types are selected before rendering.
  • Interface and union oneof alternatives now use the same stable message-field map: existing and removed subtype tags remain reserved, while new subtypes receive the next available tag.
  • Verification: all 68 elasticgraph-proto_ingestion examples pass with 100% line and branch coverage; GraphQL-schema validation, lint, spelling, and Steep are green.

@jwils
jwils force-pushed the joshuaw/protobuf-schema-generation branch from a4edbdf to 4ad8743 Compare July 8, 2026 23:40
@jwils
jwils force-pushed the joshuaw/proto-field-numbers branch from 67b8198 to f62a859 Compare July 8, 2026 23:40
@jwils
jwils force-pushed the joshuaw/protobuf-schema-generation branch from 4ad8743 to a58e00f Compare July 9, 2026 17:29
@jwils
jwils force-pushed the joshuaw/proto-field-numbers branch 2 times, most recently from 90e2af5 to 616dbfd Compare July 9, 2026 18:13
@jwils
jwils force-pushed the joshuaw/protobuf-schema-generation branch from a58e00f to 6f76222 Compare July 9, 2026 18:13
@jwils
jwils force-pushed the joshuaw/proto-field-numbers branch from 616dbfd to 74011e5 Compare July 10, 2026 15:44
@jwils
jwils force-pushed the joshuaw/protobuf-schema-generation branch from 6f76222 to 37dc7c2 Compare July 10, 2026 15:44
@jwils
jwils force-pushed the joshuaw/protobuf-schema-generation branch from 37dc7c2 to 39fb8e7 Compare July 11, 2026 13:28
@jwils
jwils force-pushed the joshuaw/proto-field-numbers branch 2 times, most recently from 075e749 to 8d6e03f Compare July 11, 2026 13:53
@jwils
jwils force-pushed the joshuaw/protobuf-schema-generation branch from 39fb8e7 to 961f145 Compare July 11, 2026 13:59
@jwils
jwils force-pushed the joshuaw/proto-field-numbers branch from 8d6e03f to 0a476c1 Compare July 11, 2026 13:59
@jwils
jwils force-pushed the joshuaw/protobuf-schema-generation branch from d838cea to e233386 Compare July 15, 2026 23:52
@jwils
jwils force-pushed the joshuaw/proto-field-numbers branch 2 times, most recently from 7a57c87 to 26fe805 Compare July 16, 2026 14:21
@jwils
jwils force-pushed the joshuaw/protobuf-schema-generation branch from e63d3d1 to 8d05d78 Compare July 17, 2026 15:05
@jwils
jwils force-pushed the joshuaw/proto-field-numbers branch from 26fe805 to 8803f5e Compare July 17, 2026 15:05
jwils added a commit that referenced this pull request Jul 20, 2026
## Why
- continue the pluggable ingestion serializer proposal after pulling
JSON Schema into its own gem
- revive the earlier protobuf prototype from #1056 on top of the new
serializer extension points

## What
- fill in the `elasticgraph-proto_ingestion` extension gem with core
generation: `schema_artifacts:dump` emits a `proto3` `schema.proto`
covering the schema's indexed types
- map built-in ElasticGraph scalars to proto types, with `t.protobuf
type:` for custom scalars (resolved via
`type_ref.with_reverted_override` so built-ins renamed with
`type_name_overrides` keep working)
- generate messages for object/interface/union types and enums (with a
zero-valued `*_UNSPECIFIED` entry), escaping proto reserved words and
wrapping lists of lists so the output stays valid
- keep `schema.proto` on public GraphQL field names; validate proto
package names
- hold extension state on a `ProtoIngestionState` container behind a
single `proto_ingestion_state` reader (matching #1281)

Field and enum value numbers are assigned sequentially in definition
order in this PR; the stacked follow-up adds the
`proto_field_numbers.yaml` sidecar that keeps them wire-stable across
schema evolution.

## Stacked follow-ups
1. this PR — core `schema.proto` generation
2. wire-stable field/enum value numbers via a `proto_field_numbers.yaml`
sidecar
3. `syntax: :proto2` support and custom file-level `headers:`
4. #1286 — enum value sourcing from existing proto enums + external
proto type references

## Verification
- `script/run_gem_specs elasticgraph-proto_ingestion` (100% line +
branch coverage at this commit)
- `script/type_check`, `script/lint`, `script/spellcheck`
- `script/quick_build` green at the stack head (whose tree is identical
to the previously reviewed single-PR revision)

## References
- #1059
- #1056
- #1079

## Update — 2026-07-10
- The current stack is #1080#1304#1306#1305#1286.
- Proto extension state now uses a mutable Struct, and keyword
package-name segments are validated without being rewritten.
- Lists of lists now raise an actionable schema error instead of
generating wrapper messages; this supersedes the earlier wrapping note
above.
---

## Update — 2026-07-15
- Interface and union messages now wrap concrete subtype messages in a
`oneof`, matching the JSON Schema `oneOf` representation.
- Concrete subtype messages omit the redundant `__typename`
discriminator.
- Proto type rendering is now stateless: the generator selects the
reachable type graph up front, then each extended type renders itself
without mutating shared traversal state.
- No-block extension coverage now completes the definitions so the
fixture remains valid under CI GraphQL-schema validation.

---

## Update — 2026-07-19
- Replaced keyword suffixing with fully qualified local message and enum
references, preserving source type and field names while disambiguating
contextual protobuf words and built-in scalar names.
- Removed the now-unnecessary keyword collision tracking and verified a
generated schema containing contextual names with `protoc` 35.1.
Base automatically changed from joshuaw/protobuf-schema-generation to main July 20, 2026 02:00
@jwils
jwils force-pushed the joshuaw/proto-field-numbers branch from 8803f5e to 178a586 Compare July 20, 2026 02:16
Comment thread elasticgraph-proto_ingestion/README.md Outdated
@jwils
jwils force-pushed the joshuaw/proto-field-numbers branch 2 times, most recently from 38809a7 to dc9a3cb Compare July 21, 2026 17:18
Field and enum value numbers were assigned sequentially in definition
order, so reordering or removing a field renumbered everything after it —
breaking wire compatibility with previously serialized data.

`schema_artifacts:dump` now reads and writes a `proto_field_numbers.yaml`
sidecar artifact:

- Existing numbers stay fixed even if field order changes; new fields get
  the lowest unused numbers.
- `schema.proto` keeps the public GraphQL field names while the sidecar
  stores private `name_in_index` overrides.
- A field renamed with `field.renamed_from` reuses its existing number
  under the new public name.
- Enum value numbers are pinned in an `enums` section; removed values keep
  their numbers reserved so they are never reused (`0` remains the
  generated `*_UNSPECIFIED` value).
- The sidecar is safe to hand-edit but strictly validated: unknown keys,
  non-integer numbers, out-of-range numbers, and collisions raise clear
  errors instead of silently reassigning numbers.

The parsed mappings are modeled by a `FieldNumberMappings` class that owns
conversion to and from the dumped artifact format.
@jwils
jwils force-pushed the joshuaw/proto-field-numbers branch from dc9a3cb to 156369c Compare July 21, 2026 17:38
Comment on lines +129 to +131
changes, and new fields get the next available numbers. Field numbers follow protobuf's
rules: they must be between 1 and 536,870,911, and the protobuf-reserved 19000-19999
range is never allocated and is rejected in mappings.

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.

Suggested change
changes, and new fields get the next available numbers. Field numbers follow protobuf's
rules: they must be between 1 and 536,870,911, and the protobuf-reserved 19000-19999
range is never allocated and is rejected in mappings.
changes, and new fields get the next available numbers.

/nit I think that's a level of detail that readers won't need to know. Being informed of that doesn't impact how anyone will use this library.


it "uses public field names in schema.proto when `name_in_index` differs" do
proto = define_proto_schema do |s|
it "assigns the lowest unused field number to a new field, rather than the number after the maximum used one" do

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.

I'm concerned about this--it has the potential to re-use an old field number that used to be used by another field that has been deleted.

If the logic grabs the lowest unused field number, what is the mechanism to prevent "recycling" old field numbers (which causes problems when deserializing old messages)?

Related to this, one thing that's unclear to me is what happens to deleted fields. Do they stick around in the artifact forever?

Comment on lines +133 to +135
The file is safe to hand-edit (e.g. when resolving a merge conflict), but it is strictly
validated: unknown keys, non-integer numbers, out-of-range numbers, and duplicate numbers
are all rejected at dump time rather than silently reassigning numbers.

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 is making me realize something: the proto_field_numbers.yaml artifact has pretty different semantics than the other artifacts and needs to be treated with more care than the others.

All the other artifacts are a pure function of the schema definition. It's safe to delete and re-recreate them at any time. This is actually a technique I've used frequently: if I had a PR which had merge conflicts on schema artifacts, the simplest approach to resolving the conflict (assuming no conflict on the Ruby schema definition) was just to delete the schema artifacts and re-generate them. It was guaranteed to be correct.

But this file can't safely be deleted: if you delete it, then you lose information about what the original field numbers were. And unlike the other schema artifacts, it's not simply an output: it's also an input into the schema generation process. The schema.proto file is a function of the Ruby definition + this file.

Another difference: the proto_field_numbers.yaml file is only used at schema artifact generation time. In contrast, the other artifacts are used by the other parts of EG.

In theory, it would be nice for all schema artifacts to share the "pure function of schema definition" property but having to maintain field numbers in the Ruby definition would be annoying so I like the idea of the proto_field_numbers.yaml file.

Here's one idea to consider: instead of treating it as a schema artifact, treat it as part of the schema definition--it just happens to be a part that EG helps you maintain (rather than needing to maintain it by hand). Concretely, here's what how I'm imagining we'd treat it differently:

  • Rather than it going in the schema artifacts directory, store it alongside the schema definition files. Specifically, as a sibling to path_to_schema. This emphasizes that it's part of the schema, not really an artifact.
  • Have the comment premable (at the top of the file) explain that it's an input to schema generation, can be updated by hand, EG helps you maintain it, and that (unlike the proper schema artifacts) you must not delete it and regenerate it or you will lose information.

Thoughts?

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.

2 participants