Repository: tableauio/tableau
Pull request #224 (2025-04-17 13:47:53)

protogen(mode): improve parsing logic for enum/stuct/union multi types

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tableauio/tableau/pull/224?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 70.57%. Comparing base [(`5d7223c`)](https://app.codecov.io/gh/tableauio/tableau/commit/5d7223c78559f7d3e70834ee4911d35cbea0358f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) to head [(`e42d038`)](https://app.codecov.io/gh/tableauio/tableau/commit/e42d03879e56285d4a74f7e08e3a796a2919d10e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #224      +/-   ##
==========================================
+ Coverage   70.48%   70.57%   +0.09%     
==========================================
  Files          79       79              
  Lines       10124    10155      +31     
==========================================
+ Hits         7136     7167      +31     
  Misses       2427     2427              
  Partials      561      561              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tableauio/tableau/pull/224?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/internal/confgen/parser.go b/internal/confgen/parser.go
index bb70e50..62b38ad 100644
--- a/internal/confgen/parser.go
+++ b/internal/confgen/parser.go
@@ -288,16 +288,24 @@ func NewSheetParser(protoPackage, locationName string, strcaseCtx strcase.Contex
 
 // NewExtendedSheetParser creates a new sheet parser with extended info.
 func NewExtendedSheetParser(protoPackage, locationName string, strcaseCtx strcase.Context, bookOpts *tableaupb.WorkbookOptions, sheetOpts *tableaupb.WorksheetOptions, extInfo *SheetParserExtInfo) *sheetParser {
-	return &sheetParser{
+	sp := &sheetParser{
 		ProtoPackage: protoPackage,
 		LocationName: locationName,
 		strcaseCtx:   strcaseCtx,
 		bookOpts:     bookOpts,
 		sheetOpts:    sheetOpts,
 		extInfo:      extInfo,
-		lookupTable:  book.ColumnLookupTable{},
-		cards:        map[string]*cardInfo{},
 	}
+	sp.reset()
+	return sp
+}
+
+// reset resets the runtime data of sheet parser for reuse
+func (sp *sheetParser) reset() {
+	sp.names = nil
+	sp.types = nil
+	sp.lookupTable = book.ColumnLookupTable{}
+	sp.cards = map[string]*cardInfo{}
 }
 
 // GetSep returns sheet-level separator.
@@ -349,6 +357,7 @@ func (sp *sheetParser) IsFieldOptional(field *Field) bool {
 }
 
 func (sp *sheetParser) Parse(protomsg proto.Message, sheet *book.Sheet) error {
+	defer sp.reset()
 	if sheet.Document != nil {
 		docParser := &documentParser{sheetParser: sp}
 		return docParser.Parse(protomsg, sheet)
diff --git a/internal/protogen/exporter.go b/internal/protogen/exporter.go
index db82b23..98a46fd 100644
--- a/internal/protogen/exporter.go
+++ b/internal/protogen/exporter.go
@@ -252,10 +252,16 @@ func (x *sheetExporter) exportUnion() error {
 		x.g.P("  message ", strings.TrimSpace(msgField.Name), " {")
 		// generate the fields
 		depth := 2
+		tagid := 1
 		for _, field := range msgField.Fields {
-			if err := x.exportField(depth, int(field.Number), field, msgField.Name); err != nil {
+			if err := x.exportField(depth, tagid, field, msgField.Name); err != nil {
 				return err
 			}
+			cross := int(field.GetOptions().GetProp().GetCross())
+			if cross < 1 {
+				cross = 1
+			}
+			tagid += cross
 		}
 		x.g.P("  }")
 	}
diff --git a/internal/protogen/sheet_mode.go b/internal/protogen/sheet_mode.go
index 3fd850f..b69c916 100644
--- a/internal/protogen/sheet_mode.go
+++ b/internal/protogen/sheet_mode.go
@@ -37,8 +37,17 @@ func extractSheetBlockTypeRow(cols []string) (name, note string, err error) {
 }
 
 func isEnumTypeBlockHeader(cols []string) bool {
-	if len(cols) >= 3 {
-		return cols[0] == colNumber && cols[1] == colName && cols[2] == colAlias
+	var containsName, containsAlias bool
+	for _, col := range cols {
+		switch col {
+		case colName:
+			containsName = true
+		case colAlias:
+			containsAlias = true
+		}
+		if containsName && containsAlias {
+			return true
+		}
 	}
 	return false
 }
@@ -69,8 +78,17 @@ func parseEnumType(ws *internalpb.Worksheet, sheet *book.Sheet, parser book.Shee
 }
 
 func isStructTypeBlockHeader(cols []string) bool {
-	if len(cols) >= 2 {
-		return cols[0] == colName && cols[1] == colType
+	var containsName, containsType bool
+	for _, col := range cols {
+		switch col {
+		case colName:
+			containsName = true
+		case colType:
+			containsType = true
+		}
+		if containsName && containsType {
+			return true
+		}
 	}
 	return false
 }
@@ -128,8 +146,19 @@ func parseStructType(ws *internalpb.Worksheet, sheet *book.Sheet, parser book.Sh
 }
 
 func isUnionTypeBlockHeader(cols []string) bool {
-	if len(cols) >= 3 {
-		return cols[0] == colName && cols[1] == colAlias && strings.HasPrefix(cols[2], colFieldPrefix)
+	var containsName, containsAlias, containsField1 bool
+	for _, col := range cols {
+		switch col {
+		case colName:
+			containsName = true
+		case colAlias:
+			containsAlias = true
+		case colFieldPrefix + "1":
+			containsField1 = true
+		}
+		if containsName && containsAlias && containsField1 {
+			return true
+		}
 	}
 	return false
 }
@@ -208,13 +237,11 @@ func parseUnionType(ws *internalpb.Worksheet, sheet *book.Sheet, parser book.She
 		var parsed bool
 		var err error
 		for cursor := 0; cursor < len(shHeader.nameRowData); cursor++ {
-			fieldNumber := cursor + 1
 			subField := &internalpb.Field{}
 			cursor, parsed, err = bp.parseField(subField, shHeader, cursor, "", "", parseroptions.Mode(tableaupb.Mode_MODE_UNION_TYPE))
 			if err != nil {
 				return wrapDebugErr(err, debugBookName, debugSheetName, shHeader, cursor)
 			}
-			subField.Number = int32(fieldNumber)
 			if parsed {
 				field.Fields = append(field.Fields, subField)
 			}


Test patch:
diff --git a/internal/protogen/protogen_test.go b/internal/protogen/protogen_test.go
index 5449704..29abd6e 100644
--- a/internal/protogen/protogen_test.go
+++ b/internal/protogen/protogen_test.go
@@ -13,6 +13,7 @@ import (
 	"github.com/tableauio/tableau/options"
 	"github.com/tableauio/tableau/proto/tableaupb"
 	"github.com/tableauio/tableau/proto/tableaupb/internalpb"
+	"google.golang.org/protobuf/proto"
 )
 
 const outdir = "./testdata/_proto/default"
@@ -122,6 +123,7 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 		name    string
 		gen     *Generator
 		args    args
+		want    []*internalpb.Worksheet
 		wantErr bool
 	}{
 		{
@@ -129,18 +131,118 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 			gen:  testgen,
 			args: args{
 				mode: tableaupb.Mode_MODE_ENUM_TYPE,
-				ws:   &internalpb.Worksheet{},
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
 				sheet: &book.Sheet{
 					Name: "ItemType",
 					Table: &book.Table{
-						MaxRow: 3,
-						MaxCol: 4,
+						MaxRow: 5,
+						MaxCol: 3,
+						Rows: [][]string{
+							{"Number", "Name", "Alias"},
+							{"0", "ITEM_TYPE_UNKNOWN", "Unknown"},
+							{"1", "ITEM_TYPE_FRUIT", "Fruit"},
+							{"2", "ITEM_TYPE_EQUIP", "Equip"},
+							{"3", "ITEM_TYPE_BOX", "Box"},
+						},
+					},
+				},
+			},
+			want: []*internalpb.Worksheet{
+				{
+					Name: "ItemType",
+					Fields: []*internalpb.Field{
+						{
+							Number: 0,
+							Name:   "ITEM_TYPE_UNKNOWN",
+							Alias:  "Unknown",
+						},
+						{
+							Number: 1,
+							Name:   "ITEM_TYPE_FRUIT",
+							Alias:  "Fruit",
+						},
+						{
+							Number: 2,
+							Name:   "ITEM_TYPE_EQUIP",
+							Alias:  "Equip",
+						},
+						{
+							Number: 3,
+							Name:   "ITEM_TYPE_BOX",
+							Alias:  "Box",
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "MODE_ENUM_TYPE_MULTI",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_ENUM_TYPE_MULTI,
+				ws:   &internalpb.Worksheet{Name: "EnumDefault"},
+				sheet: &book.Sheet{
+					Name: "EnumDefault",
+					Table: &book.Table{
+						MaxRow: 11,
+						MaxCol: 3,
 						Rows: [][]string{
+							{"ItemType", "Item's Type", ""},
 							{"Number", "Name", "Alias"},
 							{"0", "ITEM_TYPE_UNKNOWN", "Unknown"},
 							{"1", "ITEM_TYPE_FRUIT", "Fruit"},
 							{"2", "ITEM_TYPE_EQUIP", "Equip"},
 							{"3", "ITEM_TYPE_BOX", "Box"},
+							{"", "", ""},
+							{"ModeType", "Mode's Type", ""},
+							{"Alias", "Name", ""},
+							{"Pvp", "MODE_TYPE_PVP", ""},
+							{"Pve", "MODE_TYPE_PVE", ""},
+						},
+					},
+				},
+			},
+			want: []*internalpb.Worksheet{
+				{
+					Name: "ItemType",
+					Note: "Item's Type",
+					Fields: []*internalpb.Field{
+						{
+							Number: 0,
+							Name:   "ITEM_TYPE_UNKNOWN",
+							Alias:  "Unknown",
+						},
+						{
+							Number: 1,
+							Name:   "ITEM_TYPE_FRUIT",
+							Alias:  "Fruit",
+						},
+						{
+							Number: 2,
+							Name:   "ITEM_TYPE_EQUIP",
+							Alias:  "Equip",
+						},
+						{
+							Number: 3,
+							Name:   "ITEM_TYPE_BOX",
+							Alias:  "Box",
+						},
+					},
+				},
+				{
+					Name: "ModeType",
+					Note: "Mode's Type",
+					Fields: []*internalpb.Field{
+						{
+							Number: 1,
+							Name:   "MODE_TYPE_PVP",
+							Alias:  "Pvp",
+						},
+						{
+							Number: 2,
+							Name:   "MODE_TYPE_PVE",
+							Alias:  "Pve",
 						},
 					},
 				},
@@ -152,11 +254,11 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 			gen:  testgen,
 			args: args{
 				mode: tableaupb.Mode_MODE_STRUCT_TYPE,
-				ws:   &internalpb.Worksheet{},
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
 				sheet: &book.Sheet{
 					Name: "ItemType",
 					Table: &book.Table{
-						MaxRow: 3,
+						MaxRow: 4,
 						MaxCol: 2,
 						Rows: [][]string{
 							{"Name", "Type"},
@@ -167,6 +269,162 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 					},
 				},
 			},
+			want: []*internalpb.Worksheet{
+				{
+					Name: "ItemType",
+					Fields: []*internalpb.Field{
+						{
+							// Note that field numbers are not set explicitly when parsing sheets.
+							// They are auto generated when exporting to proto files finally.
+							//
+							// Number:   1,
+							Name:     "id",
+							Type:     "uint32",
+							FullType: "uint32",
+							Options: &tableaupb.FieldOptions{
+								Name: "ID",
+							},
+						},
+						{
+							// Number:   2,
+							Name:     "prop_map",
+							Type:     "map<int32, string>",
+							FullType: "map<int32, string>",
+							MapEntry: &internalpb.Field_MapEntry{
+								KeyType:       "int32",
+								ValueType:     "string",
+								ValueFullType: "string",
+							},
+							Options: &tableaupb.FieldOptions{
+								Name:   "Prop",
+								Layout: tableaupb.Layout_LAYOUT_INCELL,
+							},
+						},
+						{
+							// Number:   3,
+							Name:     "feature_list",
+							Type:     "repeated int32",
+							FullType: "repeated int32",
+							ListEntry: &internalpb.Field_ListEntry{
+								ElemType:     "int32",
+								ElemFullType: "int32",
+							},
+							Options: &tableaupb.FieldOptions{
+								Name:   "Feature",
+								Layout: tableaupb.Layout_LAYOUT_INCELL,
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "MODE_STRUCT_TYPE_MULTI",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_STRUCT_TYPE_MULTI,
+				ws:   &internalpb.Worksheet{Name: "StuuctDefault"},
+				sheet: &book.Sheet{
+					Name: "StuuctDefault",
+					Table: &book.Table{
+						MaxRow: 11,
+						MaxCol: 2,
+						Rows: [][]string{
+							{"ItemType", "Item's Type"},
+							{"Name", "Type"},
+							{"ID", "uint32"},
+							{"Prop", "map<int32, string>"},
+							{"Feature", "[]int32"},
+							{"", ""},
+							{"ModeType", "Mode's Type"},
+							{"Type", "Name"},
+							{"uint32", "ID"},
+							{"string", "Name"},
+							{"bool", "Valid"},
+						},
+					},
+				},
+			},
+			want: []*internalpb.Worksheet{
+				{
+					Name: "ItemType",
+					Note: "Item's Type",
+					Fields: []*internalpb.Field{
+						{
+							// Number:   1,
+							Name:     "id",
+							Type:     "uint32",
+							FullType: "uint32",
+							Options: &tableaupb.FieldOptions{
+								Name: "ID",
+							},
+						},
+						{
+							// Number:   2,
+							Name:     "prop_map",
+							Type:     "map<int32, string>",
+							FullType: "map<int32, string>",
+							MapEntry: &internalpb.Field_MapEntry{
+								KeyType:       "int32",
+								ValueType:     "string",
+								ValueFullType: "string",
+							},
+							Options: &tableaupb.FieldOptions{
+								Name:   "Prop",
+								Layout: tableaupb.Layout_LAYOUT_INCELL,
+							},
+						},
+						{
+							// Number:   3,
+							Name:     "feature_list",
+							Type:     "repeated int32",
+							FullType: "repeated int32",
+							ListEntry: &internalpb.Field_ListEntry{
+								ElemType:     "int32",
+								ElemFullType: "int32",
+							},
+							Options: &tableaupb.FieldOptions{
+								Name:   "Feature",
+								Layout: tableaupb.Layout_LAYOUT_INCELL,
+							},
+						},
+					},
+				},
+				{
+					Name: "ModeType",
+					Note: "Mode's Type",
+					Fields: []*internalpb.Field{
+						{
+							// Number:   1,
+							Name:     "id",
+							Type:     "uint32",
+							FullType: "uint32",
+							Options: &tableaupb.FieldOptions{
+								Name: "ID",
+							},
+						},
+						{
+							// Number:   2,
+							Name:     "name",
+							Type:     "string",
+							FullType: "string",
+							Options: &tableaupb.FieldOptions{
+								Name: "Name",
+							},
+						},
+						{
+							// Number:   3,
+							Name:     "valid",
+							Type:     "bool",
+							FullType: "bool",
+							Options: &tableaupb.FieldOptions{
+								Name: "Valid",
+							},
+						},
+					},
+				},
+			},
 			wantErr: false,
 		},
 		{
@@ -174,7 +432,7 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 			gen:  testgen,
 			args: args{
 				mode: tableaupb.Mode_MODE_UNION_TYPE,
-				ws:   &internalpb.Worksheet{},
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
 				sheet: &book.Sheet{
 					Name: "ItemType",
 					Table: &book.Table{
@@ -188,13 +446,301 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 					},
 				},
 			},
+			want: []*internalpb.Worksheet{
+				{
+					Name: "ItemType",
+					Fields: []*internalpb.Field{
+						{
+							Number: 1,
+							Name:   "PvpBattle",
+							Alias:  "SoloPVPBattle",
+							Fields: []*internalpb.Field{
+								{
+									// Number:   1,
+									Name:     "id",
+									Type:     "uint32",
+									FullType: "uint32",
+									Options: &tableaupb.FieldOptions{
+										Name: "ID",
+									},
+								},
+								{
+									// Number:   2,
+									Name:     "damage",
+									Type:     "int64",
+									FullType: "int64",
+									Options: &tableaupb.FieldOptions{
+										Name: "Damage",
+									},
+								},
+								{
+									// Number:   3,
+									Name:     "mission",
+									Type:     "Mission",
+									FullType: "Mission",
+									Options: &tableaupb.FieldOptions{
+										Name: "Mission",
+										Span: tableaupb.Span_SPAN_INNER_CELL,
+									},
+									Fields: []*internalpb.Field{
+										{
+											// Number:   1,
+											Name:     "id",
+											Type:     "uint32",
+											FullType: "uint32",
+											Options: &tableaupb.FieldOptions{
+												Name: "ID",
+											},
+										},
+										{
+											// Number:   2,
+											Name:     "level",
+											Type:     "int32",
+											FullType: "int32",
+											Options: &tableaupb.FieldOptions{
+												Name: "Level",
+											},
+										},
+									},
+								},
+							},
+						},
+						{
+							Number: 2,
+							Name:   "PveBattle",
+							Alias:  "SoloPVEBattle",
+							Fields: []*internalpb.Field{
+								{
+									// Number:   1,
+									Name:     "prop_map",
+									Type:     "map<int32, string>",
+									FullType: "map<int32, string>",
+									MapEntry: &internalpb.Field_MapEntry{
+										KeyType:       "int32",
+										ValueType:     "string",
+										ValueFullType: "string",
+									},
+									Options: &tableaupb.FieldOptions{
+										Name:   "Prop",
+										Layout: tableaupb.Layout_LAYOUT_INCELL,
+									},
+								},
+								{
+									// Number:   2,
+									Name:     "feature_list",
+									Type:     "repeated int32",
+									FullType: "repeated int32",
+									ListEntry: &internalpb.Field_ListEntry{
+										ElemType:     "int32",
+										ElemFullType: "int32",
+									},
+									Options: &tableaupb.FieldOptions{
+										Name:   "Feature",
+										Layout: tableaupb.Layout_LAYOUT_INCELL,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "MODE_UNION_TYPE_MULTI",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_UNION_TYPE_MULTI,
+				ws:   &internalpb.Worksheet{Name: "UnionDefault"},
+				sheet: &book.Sheet{
+					Name: "UnionDefault",
+					Table: &book.Table{
+						MaxRow: 9,
+						MaxCol: 5,
+						Rows: [][]string{
+							{"ItemType", "Item's Type", "", "", ""},
+							{"Name", "Alias", "Field1", "Field2", "Field3"},
+							{"PvpBattle", "SoloPVPBattle", "ID\nuint32", "Damage\nint64", "Mission\n{uint32 ID, int32 Level}Mission"},
+							{"PveBattle", "SoloPVEBattle", "Prop\nmap<int32, string>", "Feature\n[]int32"},
+							{"", "", "", "", ""},
+							{"ModeType", "Mode's Type", "", "", ""},
+							{"Field3", "Name", "Field1", "Alias", "Field2"},
+							{"", "PVP", "ID\nuint32", "PvpMode", "Difficulty\nint32"},
+							{"", "PVE", "Name\nstring", "PveMode", "Score\nint32"},
+						},
+					},
+				},
+			},
+			want: []*internalpb.Worksheet{
+				{
+					Name: "ItemType",
+					Note: "Item's Type",
+					Fields: []*internalpb.Field{
+						{
+							Number: 1,
+							Name:   "PvpBattle",
+							Alias:  "SoloPVPBattle",
+							Fields: []*internalpb.Field{
+								{
+									// Number:   1,
+									Name:     "id",
+									Type:     "uint32",
+									FullType: "uint32",
+									Options: &tableaupb.FieldOptions{
+										Name: "ID",
+									},
+								},
+								{
+									// Number:   2,
+									Name:     "damage",
+									Type:     "int64",
+									FullType: "int64",
+									Options: &tableaupb.FieldOptions{
+										Name: "Damage",
+									},
+								},
+								{
+									// Number:   3,
+									Name:     "mission",
+									Type:     "Mission",
+									FullType: "Mission",
+									Options: &tableaupb.FieldOptions{
+										Name: "Mission",
+										Span: tableaupb.Span_SPAN_INNER_CELL,
+									},
+									Fields: []*internalpb.Field{
+										{
+											// Number:   1,
+											Name:     "id",
+											Type:     "uint32",
+											FullType: "uint32",
+											Options: &tableaupb.FieldOptions{
+												Name: "ID",
+											},
+										},
+										{
+											// Number:   2,
+											Name:     "level",
+											Type:     "int32",
+											FullType: "int32",
+											Options: &tableaupb.FieldOptions{
+												Name: "Level",
+											},
+										},
+									},
+								},
+							},
+						},
+						{
+							Number: 2,
+							Name:   "PveBattle",
+							Alias:  "SoloPVEBattle",
+							Fields: []*internalpb.Field{
+								{
+									// Number:   1,
+									Name:     "prop_map",
+									Type:     "map<int32, string>",
+									FullType: "map<int32, string>",
+									MapEntry: &internalpb.Field_MapEntry{
+										KeyType:       "int32",
+										ValueType:     "string",
+										ValueFullType: "string",
+									},
+									Options: &tableaupb.FieldOptions{
+										Name:   "Prop",
+										Layout: tableaupb.Layout_LAYOUT_INCELL,
+									},
+								},
+								{
+									// Number:   2,
+									Name:     "feature_list",
+									Type:     "repeated int32",
+									FullType: "repeated int32",
+									ListEntry: &internalpb.Field_ListEntry{
+										ElemType:     "int32",
+										ElemFullType: "int32",
+									},
+									Options: &tableaupb.FieldOptions{
+										Name:   "Feature",
+										Layout: tableaupb.Layout_LAYOUT_INCELL,
+									},
+								},
+							},
+						},
+					},
+				},
+				{
+					Name: "ModeType",
+					Note: "Mode's Type",
+					Fields: []*internalpb.Field{
+						{
+							Number: 1,
+							Name:   "PVP",
+							Alias:  "PvpMode",
+							Fields: []*internalpb.Field{
+								{
+									// Number:   1,
+									Name:     "id",
+									Type:     "uint32",
+									FullType: "uint32",
+									Options: &tableaupb.FieldOptions{
+										Name: "ID",
+									},
+								},
+								{
+									// Number:   2,
+									Name:     "difficulty",
+									Type:     "int32",
+									FullType: "int32",
+									Options: &tableaupb.FieldOptions{
+										Name: "Difficulty",
+									},
+								},
+							},
+						},
+						{
+							Number: 2,
+							Name:   "PVE",
+							Alias:  "PveMode",
+							Fields: []*internalpb.Field{
+								{
+									// Number:   1,
+									Name:     "name",
+									Type:     "string",
+									FullType: "string",
+									Options: &tableaupb.FieldOptions{
+										Name: "Name",
+									},
+								},
+								{
+									// Number:   2,
+									Name:     "score",
+									Type:     "int32",
+									FullType: "int32",
+									Options: &tableaupb.FieldOptions{
+										Name: "Score",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
 			wantErr: false,
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if _, err := tt.gen.parseSpecialSheetMode(tt.args.mode, tt.args.ws, tt.args.sheet, "", ""); (err != nil) != tt.wantErr {
+			if got, err := tt.gen.parseSpecialSheetMode(tt.args.mode, tt.args.ws, tt.args.sheet, "", ""); (err != nil) != tt.wantErr {
 				t.Errorf("Generator.parseSpecialSheetMode() error = %v, wantErr %v", err, tt.wantErr)
+			} else if len(got) != len(tt.want) {
+				t.Errorf("Generator.parseSpecialSheetMode() size = %v, want size %v", len(got), len(tt.want))
+			} else {
+				for i := range got {
+					if !proto.Equal(got[i], tt.want[i]) {
+						t.Errorf("Generator.parseSpecialSheetMode()[%d] = %v\n want[%d] %v", i, got[i], i, tt.want[i])
+					}
+				}
 			}
 		})
 	}
diff --git a/test/functest/proto/default/excel__metasheet__sheet_mode.proto b/test/functest/proto/default/excel__metasheet__sheet_mode.proto
index 83c8011..1cbb435 100644
--- a/test/functest/proto/default/excel__metasheet__sheet_mode.proto
+++ b/test/functest/proto/default/excel__metasheet__sheet_mode.proto
@@ -43,6 +43,16 @@ enum BirdType {
   BIRD_TYPE_OWL = 3 [(tableau.evalue).name = "Owl"]; // Owl
 }
 
+// ColumnDisorderedEnum note
+enum ColumnDisorderedEnum {
+  option (tableau.etype) = {name:"EnumType" note:"ColumnDisorderedEnum note"};
+
+  COLUMN_DISORDERED_ENUM_INVALID = 0;
+  COLUMN_DISORDERED_ENUM_LARGE = 1 [(tableau.evalue).name = "Large"]; // Large
+  COLUMN_DISORDERED_ENUM_MEDIUM = 2 [(tableau.evalue).name = "Medium"]; // Medium
+  COLUMN_DISORDERED_ENUM_SMALL = 3 [(tableau.evalue).name = "Small"]; // Small
+}
+
 message EnumTypeReuseInBook {
   option (tableau.worksheet) = {name:"EnumTypeReuseInBook"};
 
@@ -101,6 +111,13 @@ message FruitShop {
   map<int32, string> prop_map = 2 [(tableau.field) = {name:"Prop" layout:LAYOUT_INCELL}];
 }
 
+message ColumnDisorderedStruct {
+  option (tableau.struct) = {name:"StructType" note:"ColumnDisorderedStruct note"};
+
+  int32 id = 1 [(tableau.field) = {name:"ID"}];
+  string name = 2 [(tableau.field) = {name:"Name"}];
+}
+
 message StructTypeReuseInBook {
   option (tableau.worksheet) = {name:"StructTypeReuseInBook"};
 
@@ -295,6 +312,34 @@ message BattleTarget {
   }
 }
 
+message ColumnDisorderedUnion {
+  option (tableau.union) = {name:"UnionType" note:"ColumnDisorderedUnion note"};
+
+  Type type = 9999 [(tableau.field) = {name:"Type"}];
+  oneof value {
+    option (tableau.oneof) = {note:"ColumnDisorderedUnion note" field:"Field"};
+
+    Goblin goblin = 1; // Bound to enum value: TYPE_GOBLIN.
+    Kobold kobold = 2; // Bound to enum value: TYPE_KOBOLD.
+  }
+
+  enum Type {
+    TYPE_INVALID = 0;
+    TYPE_GOBLIN = 1 [(tableau.evalue).name = "MonsterGoblin"]; // MonsterGoblin
+    TYPE_KOBOLD = 2 [(tableau.evalue).name = "MonsterKobold"]; // MonsterKobold
+  }
+
+  message Goblin {
+    int32 hp = 1 [(tableau.field) = {name:"HP"}];
+    int64 damage = 2 [(tableau.field) = {name:"Damage"}];
+  }
+  message Kobold {
+    int32 hp = 1 [(tableau.field) = {name:"HP"}];
+    string weapon = 2 [(tableau.field) = {name:"Weapon"}];
+    int32 gold = 3 [(tableau.field) = {name:"Gold"}];
+  }
+}
+
 message UnionTypeReuseInBook {
   option (tableau.worksheet) = {name:"UnionTypeReuseInBook"};
 
diff --git a/test/functest/testdata/default/excel/metasheet/SheetMode#EnumType.csv b/test/functest/testdata/default/excel/metasheet/SheetMode#EnumType.csv
index ec72cd0..77a66f6 100644
--- a/test/functest/testdata/default/excel/metasheet/SheetMode#EnumType.csv
+++ b/test/functest/testdata/default/excel/metasheet/SheetMode#EnumType.csv
@@ -17,3 +17,9 @@ Number,Name,Alias
 1,CANARY,Canary
 2,WOODPECKER,Woodpecker
 3,OWL,Owl
+,,
+ColumnDisorderedEnum,ColumnDisorderedEnum note,
+Alias,Number,Name
+Large,1,LARGE
+Medium,2,MEDIUM
+Small,3,SMALL
diff --git a/test/functest/testdata/default/excel/metasheet/SheetMode#StructType.csv b/test/functest/testdata/default/excel/metasheet/SheetMode#StructType.csv
index 3d2cdcb..3748155 100644
--- a/test/functest/testdata/default/excel/metasheet/SheetMode#StructType.csv
+++ b/test/functest/testdata/default/excel/metasheet/SheetMode#StructType.csv
@@ -14,3 +14,8 @@ FruitShop,FruitShop note
 Name,Type
 FruitType,enum<.FruitType>
 Prop,"map<int32, string>"
+,
+ColumnDisorderedStruct,ColumnDisorderedStruct note
+Type,Name
+int32,ID
+string,Name
diff --git a/test/functest/testdata/default/excel/metasheet/SheetMode#UnionType.csv b/test/functest/testdata/default/excel/metasheet/SheetMode#UnionType.csv
index 12834fb..7c41217 100644
--- a/test/functest/testdata/default/excel/metasheet/SheetMode#UnionType.csv
+++ b/test/functest/testdata/default/excel/metasheet/SheetMode#UnionType.csv
@@ -27,3 +27,13 @@ PVE,BattlePVE,"HeroID
 []int32","Dungeon
 map<int32, int64>","Boss
 {uint32 ID, int64 Damage}Boss"
+,,,,
+ColumnDisorderedUnion,ColumnDisorderedUnion note,,,
+Field3,Name,Field2,Alias,Field1
+,Goblin,"Damage
+int64",MonsterGoblin,"HP
+int32"
+"Gold
+int32",Kobold,"Weapon
+string",MonsterKobold,"HP
+int32"