feat(protogen): per-messager preserveFieldNumbers override - #441
Conversation
Field numbers only appear in the binary (binpb) wire format; JSON and txtpb encode field names, so preserveFieldNumbers is pure overhead (re-parsing all generated protos + per-field lookups) when binpb is not among the conf output formats. Gate preservation on binpb presence via a new (*ConfOutputOption).NeedBinpb() helper, which mirrors confgen's parseOutputFormats resolution (empty Formats => all formats incl. binpb). A nil conf opt (programmatic use) preserves the previous behavior. Users who reuse the generated protos for binpb outside tableau's confgen can keep preservation active by adding binpb to conf.output.formats. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest Buf updates on your PR. Results from workflow Buf CI / buf (pull_request).
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #441 +/- ##
==========================================
+ Coverage 75.63% 75.69% +0.05%
==========================================
Files 88 88
Lines 9531 9552 +21
==========================================
+ Hits 7209 7230 +21
Misses 1747 1747
Partials 575 575 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Thanks for the PR — the performance motivation is real, and the implementation itself is clean (nil-safety, conservative gating, unit tests for 1. An explicit option should not be silently vetoed by another config section
Critically, this gate is not a transparent optimization: skipping preservation changes the generated The proposed escape hatch illustrates the design smell: to keep a proto-generation behavior active, users must add 2. protogen should not depend on confgen's output configprotogen is the lower layer (schema generation); confgen consumes its output. Having the lower layer inspect the upper layer's output options to decide its own behavior is a layering inversion. Concretely: Suggestions
Minor
|
…to.output
Replaces the conf.output-coupled binpb gate (which silently vetoed an
explicit preserveFieldNumbers: true and inverted the protogen->confgen
layering) with an explicit, per-messager override that lives entirely
in proto.output.
Add ProtoOutputOption.MessagerPreserveFieldNumbers (map[string]bool,
keyed by message name, same key as conf.output.messagerFormats). A
messager in the map uses its mapped value; otherwise it falls back to
the global PreserveFieldNumbers. This lets users skip preservation for
json/txtpb-only messagers without coupling to conf.output or silently
reshuffling field numbers of binpb-consuming messagers.
preserveFieldNumbers: true
messagerPreserveFieldNumbers: { ItemConf: false }
Generator.preserveFieldNumbers(name) resolves per messager (used at
findMDFromGeneratedProtos); Generator.anyPreserveFieldNumbers() decides
whether to parse the previously generated protos at all (used at
preprocess), conservatively true if the global default or any override
is true.
Drops the ConfOutputOpt field, the NeedBinpb helper, and the info log.
Adds table-driven unit tests for both methods plus an exporter test
proving an "Item: false" override yields sequential field numbers
instead of preserved ones.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@/dev/stdin |
Evolve MessagerPreserveFieldNumbers from an exact-match map[string]bool
into an ordered list of {pattern, preserve} regex rules, so preservation
can be toggled for whole groups of messagers at once (e.g. "Temp.*|Test.*"
or "Conf$"). Rules are evaluated in order; first regex match wins, else
the global PreserveFieldNumbers default applies. Falling back to the
global default keeps the common case (no rules) unchanged.
preserveFieldNumbers: true
messagerPreserveFieldNumbers:
- { pattern: "Temp.*|Test.*", preserve: false }
Patterns are Go regexps matched (unanchored) against the message name.
They are compiled lazily and cached; an invalid pattern panics with a
clear message (consistent with the generator's other config-parse
panics), eagerly triggered from preprocess so it fails fast before any
output is regenerated.
anyPreserveFieldNumbers (the preprocess gate for parsing the previously
generated protos) is conservative: true iff the global default is true
or any rule has preserve=true.
Update unit tests with regex/precedence cases and an invalid-pattern
panic test; update the exporter integration test to use a rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@/dev/stdin |
Rename the override field to PreserveFieldNumbersRules (yaml
preserveFieldNumbersRules) so its stem matches the global
PreserveFieldNumbers option, making the relationship obvious in config.
Rename the rule struct field Pattern -> Messager (yaml messager) so the
key self-documents that the regex is matched against a messager name
rather than a generic pattern:
preserveFieldNumbers: true
preserveFieldNumbersRules:
- { messager: "Temp.*|Test.*", preserve: false }
- { messager: "^Item", preserve: true }
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trim the option/struct/method comments to the essentials: rule order, first-match-wins, unanchored regex, and the binpb relevance note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Field numbers only appear in the binpb wire format — JSON and txtpb encode field names — so
preserveFieldNumbersis pure overhead for json/txtpb-only messagers (re-parsing all generated protos + per-field lookups + reshuffling field numbers in committed.protoartifacts).This PR adds explicit, regex-based per-messager control over preservation, kept entirely inside
proto.output— no coupling toconf.output, no silent veto of an explicit user choice.Approach
PreserveFieldNumbersRulesis an ordered list of{messager, preserve}regex rules. First match wins; a messager matching no rule falls back to the globalPreserveFieldNumbers:messageris a Go regexp matched unanchored against the message name. An ordered list (not a regex-keyed map) keeps overlapping patterns deterministic.preserveFieldNumbers(name)— first matching rule wins, else global default.anyPreserveFieldNumbers()— true if the global default is true or any rule preserves; gates whether the previously-generated protos are re-parsed at all.preprocess(fails fast before regenerating output).Why this shape
The first iteration gated preservation on
conf.outputcontaining binpb. That silently vetoed an explicitpreserveFieldNumbers: true(reshuffling committed.protofield numbers behind the user's back) and inverted the protogen→confgen layering (--mode protodepended on conf settings; split runs could diverge). Per-messager rules keep the decision inproto.outputand only ever skip preservation for messagers the user explicitly matches.Non-breaking and additive — the existing
PreserveFieldNumbersbool and--preserve-field-numbersflag keep their semantics as the global default.Changes
options/options.go:PreserveFieldNumbersRules []PreserveFieldNumbersRule({Messager, Preserve}).internal/protogen/protogen.go:preserveFieldNumbers(name),anyPreserveFieldNumbers(),compiledPreserveRules(); eager validation inpreprocess.internal/protogen/exporter.go:findMDFromGeneratedProtosgates onpreserveFieldNumbers(name).{messager: "Item", preserve: false}rule yields sequential field numbers (1,2,3) instead of preserved (1,3,2).Test plan
go build ./...go test ./...(incl. functest)go vet ./options/ ./internal/protogen/ ./internal/confgen/ ./cmd/tableauc/🤖 Generated with Claude Code