ABCDEFG
1 IDNameLevelHPAttackTypeValidate
2 map<uint32,Hero>stringint32int32int32enum<HeroType>string|{validate:...
3 Hero IDHero nameHero levelMax HPBase attackHero classCEL rule
4 1Arthur50500120warrior
5 2Merlin4538090mage
6 3Lancelot55620150knight
7 4Guinevere48420110rogue
8 5Galahad60700180paladin
v0.16.0
Tableau Tableau | v0.16.0 |
Released May 29, 2026
Release Notes
v0.16.0

CEL validation via protovalidate, Buf toolchain support, structured error collection, PreserveFieldNumbers, and more.

✦ 7 Features
⬡ 9 Bug Fixes
⚠ 2 Breaking
↯ 1 Deprecated
| Go · Protobuf · Config Generation
Scroll
01

New Features

📦 01 / 07

Protobuf Editions Support

Generator now supports protobuf editions alongside proto2/proto3, with properly quoted file option values. A key step toward future-proofing generated proto files.

New Breaking
🔖 02 / 07

PreserveFieldNumbers

New option to maintain field tag number compatibility across regenerations — critical for wire format stability. Works on both regular fields and union structs.

New Option
03 / 07

CEL Validation via protovalidate

Integrated buf's protovalidate framework — declare CEL validation rules directly in spreadsheet field/worksheet properties, compiled into generated proto files.

New
🔧 04 / 07

Buf Toolchain Migration

Switched to Buf for protobuf generation, linting, and BSR publishing. CI no longer requires manual protoc install steps.

Tooling New
🔗 05 / 07

Union Field Improvements

Field numbers preserved on union structs. Fixed column ordering — definition order index (i+1) used as column suffix, ensuring stable Field1, Field2….

Enhanced
📊 06 / 07

Per-Messager Output Formats

Configure different output formats (json, txtpb, binpb) for individual messagers in confgen — fine-grained control beyond the global default.

New
🏗️ 07 / 07 · Largest Change

Structured Error Collection System

The biggest architectural change in v0.16.0 (#387 — 6,595 insertions, 119 files). Previously tableau stopped on the first parse error. Now a hierarchical Collector tree accumulates multiple errors concurrently — global → book → sheet → message — each level with its own cap. You see all errors in one run.

New Architectural · 119 files
Error Collector Hierarchy
Generator max 20
└─ Book max 10 / book
└─ Sheet max 5 / sheet
└─ Message max 3 / row
confgen: 20 → 10 → 5 → 3 (4 levels)
protogen: 10 → 5 → 3 (3 levels)
Join() assembles the full error tree
02

Bug Fixes

git log — 9 patches since v0.15.0
bc70718 protogen
Wrap bookName and sheetName correctly when parsing special sheet mode
05396e8 protogen
Add correct cell positioners for struct and union type sheet parsing
0f3049f protogen
Skip TYPE_INVALID=0 in union when enum value 0 is user-specified
7e3e4bc union
Fix incell list field properties in union message field parsing; simplify parser logic
b80914b fieldprop
Check presence correctly for incell struct / list / map / union fields
c0f520c protogen
Use fullpath when removing all protos except imports in outdir (fixes Windows)
69e0296 refer
Skip ignored rows in refer check to avoid false-positive validation errors
508d6c4 protogen
Use strings.LastIndex to correctly trim first field name from predefined struct names
1da426f log
Only output log to console after log.Init is called
03

Breaking Changes

⚠ Breaking 1 / 2

Protobuf Editions: go_package Must Be Quoted

File option values like go_package now require explicit double-quote wrapping in YAML config.

# config.yaml — fileOptions migration
- go_package: github.com/org/protoconf + go_package: '"github.com/org/protoconf"' # ↑ outer single + inner double
⚠ Breaking 2 / 2

Consistent protoFiles Parsing Refactor

protoFiles parsing unified across all code paths. Workflows relying on previous parser behavior may be affected. Re-run proto generation and verify output.

# Commit: c27cf3c — action required
// Affects: protogen + confgen pipelines // Also: CleanSlashPath for Windows paths // Also: Go 1.25 support via sonic upgrade // Action: re-generate + verify output
04

Deprecated & Refactored

🚫

Field Option cross — Deprecated

The field option cross is deprecated as of this release. Existing sheets continue to work but the option will be removed in a future version.

Refactor

JSON Parser: sonic → fastjson

Replaced bytedance/sonic with valyala/fastjson for cross-platform compatibility.

Refactor

i18n Template Quoting

Use template quote function instead of manual strconv.Quote at all call sites across the codebase.

Refactor

IsSamePath with Absolute Paths

Improved xfs.IsSamePath to resolve absolute paths, fixing subtle mismatches on Windows and symlinked directories.

Improvement

Validate Violation Error Messages

Richer, more actionable error messages for field, message, list, and map violations — easier to trace issues to source cells.

05

Dependency Updates

buf (toolchain) NEW
valyala/fastjson NEW
golangci-lint-action 8 → 9
codecov-action 5 → 6
actions/checkout 5 → 6
antchfx/xpath ↑
bufbuild/protocompile ↑
spf13/cobra ↑
go.uber.org/zap ↑
xuri/excelize ↑
06

Spreadsheet Examples

Tableau converts Excel/CSV spreadsheets into protobuf configs. Sheets use a 3-row header convention: Row 1 = field names  ·  Row 2 = field types + props  ·  Row 3 = notes. Examples use real test-data from the repository.

✦ New Feature

CEL Validation via protovalidate

Add validate: / validate_complex: / validate_message: props directly in the field-type cell (Row 2). Worksheet-level rules go in the @TABLEAU metasheet.

📄 ValidateFieldLevel — field-level validation props in Row 2
ABC
1IDNameScore
2 map<uint32, Item>|{validate:"uint32:{gt:0}" validate_complex:"map:{min_pairs:1}"} string|{validate:"string:{min_len:1 max_len:20}"} int32|{validate:"int32:{gt:0 lte:100}"}
3Item IDItem NameItem Score
41sword80
52shield95
📋 Validate#@TABLEAU — worksheet-level CEL expressions
A (Sheet)B (Mode)C (Validate)
2ValidateWorksheetLevelcel_expression:"this.item_map.size() > 0"
3ValidateStructTypeMODE_STRUCT_TYPEcel_expression:"this.begin > this.end ? 'begin must be before end' : ''"
4ValidateUnionTypeMODE_UNION_TYPEcel_expression:"this.type != 0"
▸ generated proto — field-level buf.validate annotations
// Validation rules compiled from spreadsheet props message Item {   uint32 id = 1 [(tableau.field) = {name:"ID"}, (buf.validate.field).uint32 = {gt: 0}];   string name = 2 [(tableau.field) = {name:"Name"}, (buf.validate.field).string = {min_len: 1, max_len: 20}];   int32 score = 3 [(tableau.field) = {name:"Score"}, (buf.validate.field).int32 = {gt: 0, lte: 100}]; }
✦ New Option

PreserveFieldNumbers — Safe Schema Evolution

Adding a column mid-sheet silently shifts field tag numbers — breaking binary-serialized data. Enable preserveFieldNumbers: true to lock existing fields to their original numbers. New fields get max+1.

📄 HeroConf — v1 (original)
ABC
1IDNameLevel
2map<uint32, Hero>stringint32
3Hero IDHero nameHero level
41Arthur50
📄 HeroConf — v2 (HP inserted between Name and Level)
ABCD
1IDNameHPLevel
2map<uint32, Hero>stringint32int32
3Hero IDHero nameHero HPHero level
41Arthur50050
✗ Without PreserveFieldNumbers
// Fields re-numbered in sheet order
uint32 id = 1; // ✓ unchanged
string name = 2; // ✓ unchanged
int32 hp = 3; // ← took level's old #
int32 level = 4; // ⚠ SHIFTED 3→4!
// binary data CORRUPTED
✓ With preserveFieldNumbers: true
// Existing numbers locked; new = max+1
uint32 id = 1; // ✓ preserved
string name = 2; // ✓ preserved
int32 hp = 4; // ✓ new = max+1
int32 level = 3; // ✓ preserved!
// wire format safe ✓
▸ tableauc config.yaml
proto: output: preserveFieldNumbers: true # enable preservation
💡 On first run, tableau reads existing generated .proto files in outdir to learn current field numbers — keep them before regenerating.
⬡ Bug Fix

Union Field Ordering — Column Lookup Fixed

The bug was in the confgen parser: when reading a union member's value fields, it used fd.Number() (the protobuf tag number) to construct the column name to look up. If a union member struct has non-sequential field tag numbers, the parser searched for the wrong columns and silently produced zero values. The fix uses i+1 (definition order) instead — matching how column names are always generated.

▸ Proto — Pvp union member struct with non-sequential field tag numbers
// (tableau.oneof) = {field: "Field"} → column prefix is "Field" message Pvp { int32 type = 1; // definition index i=0 → column suffix = 0+1 = 1 → TargetField1 int32 health = 4; // definition index i=1 → column suffix = 1+1 = 2 → TargetField2 (tag ≠ position!) int64 damage = 3; // definition index i=2 → column suffix = 2+1 = 3 → TargetField3 (tag ≠ position!) }
📄 TaskConf — column names are always sequential (TargetField1, TargetField2, TargetField3)
ABCDE
1 IDTargetType TargetField1 TargetField2 TargetField3
2 map<int32, Task> {union.Target}enum<union.Target.Type> union union union
3 Task IDTarget type Target field 1Target field 2Target field 3
4 1PVP 10200500
5 2PVE 35
✗ Before fix — parser used fd.Number() as column suffix
// Parsing PVP row, iterating Pvp fields:
Pvp.type (tag=1) → looked for TargetField1 → found, type=10 ✓
Pvp.health (tag=4) → looked for TargetField4 → NOT FOUND → health=0
Pvp.damage (tag=3) → looked for TargetField3 → found BUT reads wrong slot

// Result: health silently zeroed, damage wrong
✓ After fix — parser uses i+1 (definition order)
// Parsing PVP row, iterating Pvp fields:
Pvp.type (i=0, i+1=1) → looked for TargetField1 → found, type=10 ✓
Pvp.health (i=1, i+1=2) → looked for TargetField2 → found, health=200 ✓
Pvp.damage (i=2, i+1=3) → looked for TargetField3 → found, damage=500 ✓

// Result: all fields correctly parsed ✓
▸ internal/confgen/document_parser.go — the one-line fix
// Inside parseUnionMessage(), iterating fields of a union member struct: - valNodeName := unionDesc.ValueFieldName() + strconv.Itoa(int(fd.Number())) + valNodeName := unionDesc.ValueFieldName() + strconv.Itoa(i+1) // fd.Number() = proto tag (can be any int) → wrong column when non-sequential // i+1 = definition order (always 1,2,3…) → always matches sheet columns
✦ New Feature

Per-Messager Output Formats

Override output formats per messager. Supported formats: json, txtpb (text protobuf), binpb (binary protobuf). Useful when some messagers need compact binary for runtime performance while others stay in human-readable JSON or text protobuf.

📄 ItemConf — runtime-critical, needs binary
ABC
1IDNamePrice
2map<uint32, Item>stringint32
3Item IDItem nameItem price
41Sword100
📄 LevelConf — needs text protobuf for inspection
ABC
1LevelExpMaxHP
2map<int32, LevelData>int64int32
3Level numExp neededMax HP
410100
▸ tableauc config.yaml — messagerFormats (NEW in v0.16.0)
conf: output: formats: [json] # default for all messagerFormats: # per-messager overrides ItemConf: [json, binpb] # json for debug + binpb for runtime perf LevelConf: [json, txtpb] # json + txtpb for human-readable inspection
⚠ Breaking Change

Protobuf Editions: go_package Must Be Quoted

With editions support, string file options must be explicitly double-quoted in YAML config.

✗ v0.15.x config (now broken)
# tableauc config.yaml
proto:
output:
fileOptions:
go_package: github.com/org/protoconf

# Generated proto (wrong):
option go_package = github.com/org/protoconf;
# ↑ missing quotes → parse error
✓ v0.16.0 migration
# tableauc config.yaml
proto:
output:
fileOptions:
go_package: '"github.com/org/protoconf"'
# outer single + inner double quotes

# Generated proto (correct):
option go_package = "github.com/org/protoconf";
↯ Deprecated

Field Option cross — Deprecated

cross was used on a horizontal union list field to specify how many value-field columns each list element spans. cross:3 means every element in the list occupies exactly 3 consecutive value-field columns. The option is deprecated as of v0.16.0 — existing sheets still work but it will be removed in a future version.

🏗️ Architectural

Structured Error Collector — See All Errors, Not Just the First

The new xerrors.Collector tree collects multiple parse errors concurrently across the full pipeline. Each level has its own cap — parsing stops only when a level's budget is exhausted, not on the very first error.

📄 Collector#ItemConf.csv — two invalid cells in the same row
ABC
1IDNumPrice
2uint32int32int32
3Item's IDItem's numItem's price
41 xyz bad_price
📄 Collector#ShopConf.csv — errors across two rows
AB
1ShopIDPrice
2uint32int32
3Shop's IDGoods's price
41bad_price
5bad_id200
✗ Old behavior (v0.15.x) — stops at first error
$ tableauc gen ...

error[E2012]: invalid syntax of numerical value
Workbook: Collector#*.csv
Worksheet: ItemConf
DataCellPos: B4
DataCell: xyz
Reason: "xyz" cannot be parsed to int32

// ← STOPS. Fix xyz, run again,
// discover bad_price, fix it, run again…
// painful iteration cycle.
✓ New behavior (v0.16.0) — all errors at once
$ tableauc gen ...

[1] error[E2012]: invalid syntax…
Workbook: Collector#*.csv
...

[2] error[E2012]: invalid syntax…
Workbook: Collector#*.csv
...

[3] error[E2012]: invalid syntax…
Workbook: Collector#*.csv
...

// Fix ALL 3 issues in one pass ✓
▸ confgen 4-level collector hierarchy (protogen uses 3 levels: 10→5→3, no message level)
// Each level has its own cap; Collect(err) increments self + ALL ancestors gen.collector = NewCollector(20) // global: all workbooks bookCollector = gen.collector.NewChild(10) // per workbook sheetCollector = bookCollector.NewChild(5) // per sheet messageCollector = sheetCollector.NewChild(3) // per row/message   // Concurrent workbooks via Group (cancels ctx when full): group = gen.collector.NewGroup(ctx) group.Go(func(ctx) { return gen.convertTable(fd) }) // one goroutine per workbook err = group.Wait() // returns joined error tree or nil
💡 Cap behavior with overflow (confgen): A sheet with 12 bad rows reports only the first 5 errors per sheet (up to 3 per row). Parsing skips remaining rows once the sheet cap is reached. The global cap of 20 prevents runaway output across all concurrent workbooks.