Repository: tableauio/tableau
Pull request #229 (2025-04-22 08:03:21)

protogen(table): check number/name/alias fields' uniqueness for enum/union types

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tableauio/tableau/pull/229?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) Report
Attention: Patch coverage is `94.11765%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 70.86%. Comparing base [(`39677cf`)](https://app.codecov.io/gh/tableauio/tableau/commit/39677cf81e4e78974da98c8fc556ed97836bb002?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) to head [(`e7e7281`)](https://app.codecov.io/gh/tableauio/tableau/commit/e7e72814b0f5835b549b173c3a43fbfcfdaa4ad2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio).

| [Files with missing lines](https://app.codecov.io/gh/tableauio/tableau/pull/229?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) | Patch % | Lines |
|---|---|---|
| [internal/importer/excel.go](https://app.codecov.io/gh/tableauio/tableau/pull/229?src=pr&el=tree&filepath=internal%2Fimporter%2Fexcel.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-aW50ZXJuYWwvaW1wb3J0ZXIvZXhjZWwuZ28=) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/tableauio/tableau/pull/229?src=pr&el=tree&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     #229      +/-   ##
==========================================
+ Coverage   70.74%   70.86%   +0.12%     
==========================================
  Files          79       79              
  Lines       10196    10197       +1     
==========================================
+ Hits         7213     7226      +13     
+ Misses       2422     2414       -8     
+ Partials      561      557       -4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tableauio/tableau/pull/229?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 62b38ad..e1c5e6a 100644
--- a/internal/confgen/parser.go
+++ b/internal/confgen/parser.go
@@ -269,7 +269,8 @@ type cardInfo struct {
 type uniqueField struct {
 	fd protoreflect.FieldDescriptor
 	// value set: map[string]bool
-	values map[string]bool
+	values   map[string]bool
+	optional bool
 }
 
 // SheetParserExtInfo is the extended info for refer check and so on.
@@ -610,6 +611,8 @@ func (sp *sheetParser) deduceKeyUnique(fieldLayout tableaupb.Layout, md protoref
 // checkSubFieldUnique checks whether the map value's or list element's sub-field is unique.
 // If an error occured, it will return the field option name which has the duplicated value.
 //
+// The uniqueness check ignores optional fields with default value.
+//
 // # Performance improvement
 //
 //  1. parse sub fields metadata only once, and cache it for later use.
@@ -646,27 +649,28 @@ func (sp *sheetParser) checkSubFieldUnique(field *Field, cardPrefix string, newV
 				continue
 			}
 			if fieldprop.RequireUnique(subField.opts.Prop) {
-				value := fmt.Sprint(newValue.Message().Get(fd))
 				info.uniqueFields[subField.opts.GetName()] = &uniqueField{
-					fd: subField.fd,
-					values: map[string]bool{
-						value: true,
-					},
+					fd:       subField.fd,
+					values:   map[string]bool{},
+					optional: sp.IsFieldOptional(subField),
 				}
 			}
 		}
 		// add new unique value
 		sp.cards[cardPrefix] = info
-	} else {
-		for name, field := range info.uniqueFields {
-			val := fmt.Sprint(newValue.Message().Get(field.fd))
-			if field.values[fmt.Sprint(val)] {
-				dupName, err = name, xerrors.E2022(field.fd.Name(), val)
-				return dupName, err
-			}
-			// add new unique value
-			field.values[val] = true
+	}
+	for name, field := range info.uniqueFields {
+		if field.optional && !newValue.Message().Has(field.fd) {
+			// ignore optional fields with default value
+			continue
+		}
+		val := fmt.Sprint(newValue.Message().Get(field.fd))
+		if field.values[fmt.Sprint(val)] {
+			dupName, err = name, xerrors.E2022(field.fd.Name(), val)
+			return dupName, err
 		}
+		// add new unique value
+		field.values[val] = true
 	}
 	return dupName, nil
 }
diff --git a/internal/confgen/table_parser.go b/internal/confgen/table_parser.go
index d8b1538..a0621a4 100644
--- a/internal/confgen/table_parser.go
+++ b/internal/confgen/table_parser.go
@@ -87,7 +87,7 @@ func (sp *tableParser) Parse(protomsg proto.Message, sheet *book.Sheet) error {
 		typeRow := header.TypeRow - 1
 		var prev *book.RowCells
 		for row := header.DataRow - 1; row < sheet.Table.MaxRow; row++ {
-			curr := book.NewRowCells(row, prev, sheet.Name)
+			curr := book.NewRowCells(row+sheet.Table.Options.RowOffset, prev, sheet.Name)
 			for col := 0; col < sheet.Table.MaxCol; col++ {
 				if row == header.DataRow-1 {
 					nameCell, err := sheet.Table.Cell(nameRow, col)
diff --git a/internal/importer/book/options.go b/internal/importer/book/options.go
new file mode 100644
index 0000000..cc90909
--- /dev/null
+++ b/internal/importer/book/options.go
@@ -0,0 +1,27 @@
+package book
+
+type TableOptions struct {
+	// RowOffset is the row index of the first row in Rows.
+	// The current Rows may be part of a multi enum/struct/union sheet,
+	// so we need to know the offset to calculate the correct row number.
+	RowOffset int
+}
+
+// Option is the functional option type.
+type TableOption func(*TableOptions)
+
+// RowOffset sets the offset of table's first row to the whole sheet.
+// For non-multi sheet modes, the offset is always zero.
+func RowOffset(offset int) TableOption {
+	return func(opts *TableOptions) {
+		opts.RowOffset = offset
+	}
+}
+
+func parseTableOptions(setters ...TableOption) *TableOptions {
+	opts := &TableOptions{}
+	for _, setter := range setters {
+		setter(opts)
+	}
+	return opts
+}
diff --git a/internal/importer/book/sheet.go b/internal/importer/book/sheet.go
index 83b413a..1f6ba25 100644
--- a/internal/importer/book/sheet.go
+++ b/internal/importer/book/sheet.go
@@ -51,10 +51,10 @@ type Sheet struct {
 }
 
 // NewTableSheet creates a new Sheet with a table.
-func NewTableSheet(name string, rows [][]string) *Sheet {
+func NewTableSheet(name string, rows [][]string, setters ...TableOption) *Sheet {
 	return &Sheet{
 		Name:  name,
-		Table: NewTable(rows),
+		Table: NewTable(rows, setters...),
 	}
 }
 
diff --git a/internal/importer/book/table.go b/internal/importer/book/table.go
index 6c1abb7..ad9e08a 100644
--- a/internal/importer/book/table.go
+++ b/internal/importer/book/table.go
@@ -14,12 +14,15 @@ type Table struct {
 	MaxRow int
 	MaxCol int
 	Rows   [][]string // 2D array strings
+
+	Options TableOptions
 }
 
 // NewTable creates a new Table.
-func NewTable(rows [][]string) *Table {
+func NewTable(rows [][]string, setters ...TableOption) *Table {
 	maxRow := len(rows)
 	maxCol := 0
+	opts := parseTableOptions(setters...)
 	// NOTE: different rows may have different lengths,
 	// and we need to find the max col.
 	for _, row := range rows {
@@ -29,9 +32,10 @@ func NewTable(rows [][]string) *Table {
 		}
 	}
 	return &Table{
-		MaxRow: maxRow,
-		MaxCol: maxCol,
-		Rows:   rows,
+		MaxRow:  maxRow,
+		MaxCol:  maxCol,
+		Rows:    rows,
+		Options: *opts,
 	}
 }
 
diff --git a/internal/protogen/protogen.go b/internal/protogen/protogen.go
index 897eba6..aed00e1 100644
--- a/internal/protogen/protogen.go
+++ b/internal/protogen/protogen.go
@@ -579,7 +579,7 @@ func (gen *Generator) extractTypeInfoFromSpecialSheetMode(mode tableaupb.Mode, s
 				blockBeginRow := row
 				block, blockEndRow := sheet.Table.ExtractBlock(blockBeginRow)
 				row = blockEndRow // skip row to next block
-				subSheet := book.NewTableSheet(sheet.Name, block)
+				subSheet := book.NewTableSheet(sheet.Name, block, book.RowOffset(blockBeginRow))
 				if err := extractStructTypeInfo(subSheet, typeName, parentFilename, parser, gen); err != nil {
 					return err
 				}
@@ -604,7 +604,7 @@ func (gen *Generator) extractTypeInfoFromSpecialSheetMode(mode tableaupb.Mode, s
 				blockBeginRow := row
 				block, blockEndRow := sheet.Table.ExtractBlock(blockBeginRow)
 				row = blockEndRow // skip row to next block
-				subSheet := book.NewTableSheet(sheet.Name, block)
+				subSheet := book.NewTableSheet(sheet.Name, block, book.RowOffset(blockBeginRow))
 				if err := extractUnionTypeInfo(subSheet, typeName, parentFilename, parser, gen); err != nil {
 					return err
 				}
@@ -650,7 +650,7 @@ func (gen *Generator) parseSpecialSheetMode(mode tableaupb.Mode, ws *internalpb.
 				}
 				block, blockEndRow := sheet.Table.ExtractBlock(blockBeginRow)
 				row = blockEndRow // skip row to next block
-				subSheet := book.NewTableSheet(subWs.Name, block)
+				subSheet := book.NewTableSheet(subWs.Name, block, book.RowOffset(blockBeginRow))
 				if err := parseEnumType(subWs, subSheet, parser, gen); err != nil {
 					return nil, err
 				}
@@ -681,7 +681,7 @@ func (gen *Generator) parseSpecialSheetMode(mode tableaupb.Mode, ws *internalpb.
 				}
 				block, blockEndRow := sheet.Table.ExtractBlock(blockBeginRow)
 				row = blockEndRow // skip row to next block
-				subSheet := book.NewTableSheet(subWs.Name, block)
+				subSheet := book.NewTableSheet(subWs.Name, block, book.RowOffset(blockBeginRow))
 				if err := parseStructType(subWs, subSheet, parser, gen, debugBookName, debugSheetName); err != nil {
 					return nil, err
 				}
@@ -712,7 +712,7 @@ func (gen *Generator) parseSpecialSheetMode(mode tableaupb.Mode, ws *internalpb.
 				}
 				block, blockEndRow := sheet.Table.ExtractBlock(blockBeginRow)
 				row = blockEndRow // skip row to next block
-				subSheet := book.NewTableSheet(subWs.Name, block)
+				subSheet := book.NewTableSheet(subWs.Name, block, book.RowOffset(blockBeginRow))
 				if err := parseUnionType(subWs, subSheet, parser, gen, debugBookName, debugSheetName); err != nil {
 					return nil, err
 				}
diff --git a/options/options.go b/options/options.go
index b6fc9b5..ecc8f97 100644
--- a/options/options.go
+++ b/options/options.go
@@ -152,7 +152,7 @@ type ProtoInputOption struct {
 	//
 	// The first-pass mode can be:
 	//
-	//	- "": default mode, parse based on specified config files.
+	//  - "": default mode, parse based on specified config files.
 	//  - "normal": parse based on all config files.
 	//  - "advanced": parse based on all previous generated proto files.
 	//
diff --git a/proto/tableau/protobuf/internal/metabook.proto b/proto/tableau/protobuf/internal/metabook.proto
index 42c73ab..0a8e903 100644
--- a/proto/tableau/protobuf/internal/metabook.proto
+++ b/proto/tableau/protobuf/internal/metabook.proto
@@ -32,9 +32,9 @@ message Metasheet {
   string subsep = 13 [(tableau.field) = {name:"Subsep" prop:{optional:true}}];
   // merge multiple sheets with same schema to one.
   // each element is:
-  // - a workbook name or Glob(https://pkg.go.dev/path/filepath#Glob) to merge (relative to this workbook): <Workbook>,
+  //  - a workbook name or Glob(https://pkg.go.dev/path/filepath#Glob) to merge (relative to this workbook): <Workbook>,
   //   then the sheet name is the same as this sheet.
-  // - or a workbook name (relative to this workbook) with a worksheet name: <Workbook>#<Worksheet>.
+  //  - or a workbook name (relative to this workbook) with a worksheet name: <Workbook>#<Worksheet>.
   repeated string merger = 14 [(tableau.field) = {name:"Merger" layout:LAYOUT_INCELL prop:{optional:true}}];
   // Tableau will merge adjacent rows with the same key. If the key cell is not set,
   // it will be treated as the same as the most nearest key above the same column.
@@ -56,9 +56,9 @@ message Metasheet {
   tableau.Mode mode = 18 [(tableau.field) = {name:"Mode" prop:{optional:true}}];
   // Scatter converts sheets separately with same schema.
   // each element is:
-  // - a workbook name or Glob(https://pkg.go.dev/path/filepath#Glob) which is relative to this workbook: <Workbook>,
+  //  - a workbook name or Glob(https://pkg.go.dev/path/filepath#Glob) which is relative to this workbook: <Workbook>,
   //   then the sheet name is the same as this sheet.
-  // - or a workbook name which is relative to this workbook with a worksheet name: <Workbook>#<Worksheet>.
+  //  - or a workbook name which is relative to this workbook with a worksheet name: <Workbook>#<Worksheet>.
   repeated string scatter = 19 [(tableau.field) = {name:"Scatter" layout:LAYOUT_INCELL prop:{optional:true}}];
   // Whether all fields in this sheet are optional (field name existence).
   // If set to true, then:
@@ -100,8 +100,8 @@ message Metasheet {
   // Generated APIs are:
   //
   // C++:
-  // - const std::vector<const STRUCT_TYPE*>& Find<IndexName>(INDEX_TYPE index) const;
-  // - const STRUCT_TYPE* FindFirst<IndexName>(INDEX_TYPE index);
+  //  - const std::vector<const STRUCT_TYPE*>& Find<IndexName>(INDEX_TYPE index) const;
+  //  - const STRUCT_TYPE* FindFirst<IndexName>(INDEX_TYPE index);
   string index = 51 [(tableau.field) = {name:"Index" prop:{optional:true}}];
 
   // Specify loader language options.
@@ -125,9 +125,9 @@ message EnumDescriptor {
 
   repeated Value values = 1 [(tableau.field) = {layout:LAYOUT_VERTICAL}];
   message Value {
-    optional int32 number = 1 [(tableau.field) = {name:"Number" prop:{optional:true}}];
-    string name = 2 [(tableau.field) = {name:"Name"}];
-    string alias = 3 [(tableau.field) = {name:"Alias"}];
+    optional int32 number = 1 [(tableau.field) = {name:"Number" prop:{optional:true unique:true}}];
+    string name = 2 [(tableau.field) = {name:"Name" prop:{unique:true}}];
+    string alias = 3 [(tableau.field) = {name:"Alias" prop:{unique:true}}];
   }
 }
 
@@ -140,7 +140,7 @@ message StructDescriptor {
 
   repeated Field fields = 1 [(tableau.field) = {layout:LAYOUT_VERTICAL}];
   message Field {
-    string name = 1 [(tableau.field) = {name:"Name"}];
+    string name = 1 [(tableau.field) = {name:"Name" prop:{unique:true}}];
     string type = 2 [(tableau.field) = {name:"Type"}];
   }
 }
@@ -154,11 +154,11 @@ message UnionDescriptor {
 
   repeated Value values = 1 [(tableau.field) = {layout:LAYOUT_VERTICAL}];
   message Value {
-    optional int32 number = 1 [(tableau.field) = {name:"Number" prop:{optional:true}}];
+    optional int32 number = 1 [(tableau.field) = {name:"Number" prop:{optional:true unique:true}}];
     // This is message type name, and the corresponding enum value name
     // is generated as: "TYPE_" + strcase.ToScreamingSnake(name).
-    string name = 2 [(tableau.field) = {name:"Name"}];
-    string alias = 3 [(tableau.field) = {name:"Alias"}];
+    string name = 2 [(tableau.field) = {name:"Name" prop:{unique:true}}];
+    string alias = 3 [(tableau.field) = {name:"Alias" prop:{unique:true}}];
     repeated string fields = 4 [(tableau.field) = {name:"Field" layout:LAYOUT_HORIZONTAL prop:{fixed:true}}];
   }
 }
diff --git a/proto/tableaupb/internalpb/metabook.pb.go b/proto/tableaupb/internalpb/metabook.pb.go
index 3e98b53..4c42a96 100644
--- a/proto/tableaupb/internalpb/metabook.pb.go
+++ b/proto/tableaupb/internalpb/metabook.pb.go
@@ -160,8 +160,8 @@ type Metasheet struct {
 	// Generated APIs are:
 	//
 	// C++:
-	// - const std::vector<const STRUCT_TYPE*>& Find<IndexName>(INDEX_TYPE index) const;
-	// - const STRUCT_TYPE* FindFirst<IndexName>(INDEX_TYPE index);
+	//   - const std::vector<const STRUCT_TYPE*>& Find<IndexName>(INDEX_TYPE index) const;
+	//   - const STRUCT_TYPE* FindFirst<IndexName>(INDEX_TYPE index);
 	Index string `protobuf:"bytes,51,opt,name=index,proto3" json:"index,omitempty"`
 	// Specify loader language options.
 	// Valid keys are: OrderedMap, Index.
@@ -848,53 +848,55 @@ var file_tableau_protobuf_internal_metabook_proto_rawDesc = []byte{
 	0x4c, 0x61, 0x6e, 0x67, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
 	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
 	0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xe1, 0x01, 0x0a,
+	0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xeb, 0x01, 0x0a,
 	0x0e, 0x45, 0x6e, 0x75, 0x6d, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12,
 	0x3e, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
 	0x1e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x44,
 	0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42,
 	0x06, 0x82, 0xb5, 0x18, 0x02, 0x20, 0x01, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a,
-	0x84, 0x01, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2d, 0x0a, 0x06, 0x6e, 0x75, 0x6d,
-	0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x10, 0x82, 0xb5, 0x18, 0x0c, 0x0a,
-	0x06, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x7a, 0x02, 0x58, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6e,
-	0x75, 0x6d, 0x62, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x1e, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x82, 0xb5, 0x18, 0x06, 0x0a, 0x04, 0x4e, 0x61,
-	0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x05, 0x61, 0x6c, 0x69, 0x61,
-	0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0x82, 0xb5, 0x18, 0x07, 0x0a, 0x05, 0x41,
-	0x6c, 0x69, 0x61, 0x73, 0x52, 0x05, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x42, 0x09, 0x0a, 0x07, 0x5f,
-	0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x3a, 0x08, 0x82, 0xb5, 0x18, 0x04, 0x10, 0x01, 0x28, 0x02,
-	0x22, 0xa7, 0x01, 0x0a, 0x10, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x44, 0x65, 0x73, 0x63, 0x72,
-	0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x40, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18,
-	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
-	0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f,
-	0x72, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x06, 0x82, 0xb5, 0x18, 0x02, 0x20, 0x01, 0x52,
-	0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x47, 0x0a, 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64,
-	0x12, 0x1e, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a,
-	0x82, 0xb5, 0x18, 0x06, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
-	0x12, 0x1e, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a,
-	0x82, 0xb5, 0x18, 0x06, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
-	0x3a, 0x08, 0x82, 0xb5, 0x18, 0x04, 0x10, 0x01, 0x28, 0x02, 0x22, 0x8e, 0x02, 0x0a, 0x0f, 0x55,
-	0x6e, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x3f,
-	0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f,
-	0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x55, 0x6e, 0x69, 0x6f, 0x6e, 0x44,
-	0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42,
-	0x06, 0x82, 0xb5, 0x18, 0x02, 0x20, 0x01, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a,
-	0xaf, 0x01, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2d, 0x0a, 0x06, 0x6e, 0x75, 0x6d,
-	0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x10, 0x82, 0xb5, 0x18, 0x0c, 0x0a,
-	0x06, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x7a, 0x02, 0x58, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6e,
-	0x75, 0x6d, 0x62, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x1e, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x82, 0xb5, 0x18, 0x06, 0x0a, 0x04, 0x4e, 0x61,
-	0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x05, 0x61, 0x6c, 0x69, 0x61,
-	0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0b, 0x82, 0xb5, 0x18, 0x07, 0x0a, 0x05, 0x41,
-	0x6c, 0x69, 0x61, 0x73, 0x52, 0x05, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x29, 0x0a, 0x06, 0x66,
-	0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x42, 0x11, 0x82, 0xb5, 0x18,
-	0x0d, 0x0a, 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x02, 0x7a, 0x02, 0x30, 0x01, 0x52, 0x06,
-	0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65,
-	0x72, 0x3a, 0x08, 0x82, 0xb5, 0x18, 0x04, 0x10, 0x01, 0x28, 0x02, 0x42, 0x39, 0x5a, 0x37, 0x67,
-	0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61,
-	0x75, 0x69, 0x6f, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2f, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x70, 0x62, 0x2f, 0x69, 0x6e, 0x74, 0x65,
-	0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x8e, 0x01, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x6e, 0x75, 0x6d,
+	0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x12, 0x82, 0xb5, 0x18, 0x0e, 0x0a,
+	0x06, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x7a, 0x04, 0x10, 0x01, 0x58, 0x01, 0x48, 0x00, 0x52,
+	0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x04, 0x6e, 0x61,
+	0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0e, 0x82, 0xb5, 0x18, 0x0a, 0x0a, 0x04,
+	0x4e, 0x61, 0x6d, 0x65, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x25,
+	0x0a, 0x05, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0f, 0x82,
+	0xb5, 0x18, 0x0b, 0x0a, 0x05, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x05,
+	0x61, 0x6c, 0x69, 0x61, 0x73, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72,
+	0x3a, 0x08, 0x82, 0xb5, 0x18, 0x04, 0x10, 0x01, 0x28, 0x02, 0x22, 0xab, 0x01, 0x0a, 0x10, 0x53,
+	0x74, 0x72, 0x75, 0x63, 0x74, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12,
+	0x40, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x20, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63,
+	0x74, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x46, 0x69, 0x65, 0x6c,
+	0x64, 0x42, 0x06, 0x82, 0xb5, 0x18, 0x02, 0x20, 0x01, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64,
+	0x73, 0x1a, 0x4b, 0x0a, 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x22, 0x0a, 0x04, 0x6e, 0x61,
+	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0e, 0x82, 0xb5, 0x18, 0x0a, 0x0a, 0x04,
+	0x4e, 0x61, 0x6d, 0x65, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e,
+	0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0x82, 0xb5,
+	0x18, 0x06, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x3a, 0x08,
+	0x82, 0xb5, 0x18, 0x04, 0x10, 0x01, 0x28, 0x02, 0x22, 0x98, 0x02, 0x0a, 0x0f, 0x55, 0x6e, 0x69,
+	0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x3f, 0x0a, 0x06,
+	0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69,
+	0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x55, 0x6e, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73,
+	0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x06, 0x82,
+	0xb5, 0x18, 0x02, 0x20, 0x01, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0xb9, 0x01,
+	0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65,
+	0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x12, 0x82, 0xb5, 0x18, 0x0e, 0x0a, 0x06, 0x4e,
+	0x75, 0x6d, 0x62, 0x65, 0x72, 0x7a, 0x04, 0x10, 0x01, 0x58, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6e,
+	0x75, 0x6d, 0x62, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0e, 0x82, 0xb5, 0x18, 0x0a, 0x0a, 0x04, 0x4e, 0x61,
+	0x6d, 0x65, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x05,
+	0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0f, 0x82, 0xb5, 0x18,
+	0x0b, 0x0a, 0x05, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x7a, 0x02, 0x10, 0x01, 0x52, 0x05, 0x61, 0x6c,
+	0x69, 0x61, 0x73, 0x12, 0x29, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x04, 0x20,
+	0x03, 0x28, 0x09, 0x42, 0x11, 0x82, 0xb5, 0x18, 0x0d, 0x0a, 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64,
+	0x20, 0x02, 0x7a, 0x02, 0x30, 0x01, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x42, 0x09,
+	0x0a, 0x07, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x3a, 0x08, 0x82, 0xb5, 0x18, 0x04, 0x10,
+	0x01, 0x28, 0x02, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
+	0x6d, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x69, 0x6f, 0x2f, 0x74, 0x61, 0x62, 0x6c,
+	0x65, 0x61, 0x75, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61,
+	0x75, 0x70, 0x62, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x62, 0x06,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (


Test patch:
diff --git a/internal/protogen/protogen_test.go b/internal/protogen/protogen_test.go
index 9e6b0a1..96933a3 100644
--- a/internal/protogen/protogen_test.go
+++ b/internal/protogen/protogen_test.go
@@ -7,12 +7,14 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"github.com/tableauio/tableau/format"
 	"github.com/tableauio/tableau/internal/importer/book"
 	"github.com/tableauio/tableau/internal/x/xfs"
 	"github.com/tableauio/tableau/options"
 	"github.com/tableauio/tableau/proto/tableaupb"
 	"github.com/tableauio/tableau/proto/tableaupb/internalpb"
+	"github.com/tableauio/tableau/xerrors"
 	"google.golang.org/protobuf/proto"
 )
 
@@ -184,6 +186,7 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 		args    args
 		want    []*internalpb.Worksheet
 		wantErr bool
+		errcode string
 	}{
 		{
 			name: "MODE_ENUM_TYPE",
@@ -235,6 +238,146 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name: "MODE_ENUM_TYPE no number",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_ENUM_TYPE,
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
+				sheet: &book.Sheet{
+					Name: "ItemType",
+					Table: &book.Table{
+						MaxRow: 4,
+						MaxCol: 2,
+						Rows: [][]string{
+							{"Name", "Alias"},
+							{"ITEM_TYPE_FRUIT", "Fruit"},
+							{"ITEM_TYPE_EQUIP", "Equip"},
+							{"ITEM_TYPE_BOX", "Box"},
+						},
+					},
+				},
+			},
+			want: []*internalpb.Worksheet{
+				{
+					Name: "ItemType",
+					Fields: []*internalpb.Field{
+						{
+							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 dup number",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_ENUM_TYPE,
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
+				sheet: &book.Sheet{
+					Name: "ItemType",
+					Table: &book.Table{
+						MaxRow: 5,
+						MaxCol: 3,
+						Rows: [][]string{
+							{"Number", "Name", "Alias"},
+							{"0", "ITEM_TYPE_UNKNOWN", "Unknown"},
+							{"1", "ITEM_TYPE_FRUIT", "Fruit"},
+							{"2", "ITEM_TYPE_EQUIP", "Equip"},
+							{"2", "ITEM_TYPE_BOX", "Box"}, // duplicate
+						},
+					},
+				},
+			},
+			wantErr: true,
+			errcode: "E2022",
+		},
+		{
+			name: "MODE_ENUM_TYPE dup zero number",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_ENUM_TYPE,
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
+				sheet: &book.Sheet{
+					Name: "ItemType",
+					Table: &book.Table{
+						MaxRow: 5,
+						MaxCol: 3,
+						Rows: [][]string{
+							{"Number", "Name", "Alias"},
+							{"0", "ITEM_TYPE_UNKNOWN", "Unknown"},
+							{"0", "ITEM_TYPE_FRUIT", "Fruit"}, // duplicate
+							{"1", "ITEM_TYPE_EQUIP", "Equip"},
+							{"2", "ITEM_TYPE_BOX", "Box"},
+						},
+					},
+				},
+			},
+			wantErr: true,
+			errcode: "E2022",
+		},
+		{
+			name: "MODE_ENUM_TYPE dup name",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_ENUM_TYPE,
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
+				sheet: &book.Sheet{
+					Name: "ItemType",
+					Table: &book.Table{
+						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_EQUIP", "Box"}, // duplicate
+						},
+					},
+				},
+			},
+			wantErr: true,
+			errcode: "E2022",
+		},
+		{
+			name: "MODE_ENUM_TYPE dup alias",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_ENUM_TYPE,
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
+				sheet: &book.Sheet{
+					Name: "ItemType",
+					Table: &book.Table{
+						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", "Equip"}, // duplicate
+						},
+					},
+				},
+			},
+			wantErr: true,
+			errcode: "E2022",
+		},
 		{
 			name: "MODE_ENUM_TYPE_MULTI",
 			gen:  testgen,
@@ -378,6 +521,29 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name: "MODE_STRUCT_TYPE dup name",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_STRUCT_TYPE,
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
+				sheet: &book.Sheet{
+					Name: "ItemType",
+					Table: &book.Table{
+						MaxRow: 4,
+						MaxCol: 2,
+						Rows: [][]string{
+							{"Name", "Type"},
+							{"ID", "uint32"},
+							{"Prop", "map<int32, string>"},
+							{"Prop", "[]int32"}, // dupliacte
+						},
+					},
+				},
+			},
+			wantErr: true,
+			errcode: "E2022",
+		},
 		{
 			name: "MODE_STRUCT_TYPE_MULTI",
 			gen:  testgen,
@@ -605,6 +771,132 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name: "MODE_UNION_TYPE with number",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_UNION_TYPE,
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
+				sheet: &book.Sheet{
+					Name: "ItemType",
+					Table: &book.Table{
+						MaxRow: 3,
+						MaxCol: 4,
+						Rows: [][]string{
+							{"Number", "Name", "Alias", "Field1"},
+							{"2", "PvpBattle", "SoloPVPBattle", "ID\nuint32"},
+							{"5", "PveBattle", "SoloPVEBattle", "Name\nstring"},
+						},
+					},
+				},
+			},
+			want: []*internalpb.Worksheet{
+				{
+					Name: "ItemType",
+					Fields: []*internalpb.Field{
+						{
+							Number: 2,
+							Name:   "PvpBattle",
+							Alias:  "SoloPVPBattle",
+							Fields: []*internalpb.Field{
+								{
+									// Number:   1,
+									Name:     "id",
+									Type:     "uint32",
+									FullType: "uint32",
+									Options: &tableaupb.FieldOptions{
+										Name: "ID",
+									},
+								},
+							},
+						},
+						{
+							Number: 5,
+							Name:   "PveBattle",
+							Alias:  "SoloPVEBattle",
+							Fields: []*internalpb.Field{
+								{
+									// Number:   1,
+									Name:     "name",
+									Type:     "string",
+									FullType: "string",
+									Options: &tableaupb.FieldOptions{
+										Name: "Name",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "MODE_UNION_TYPE dup number",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_UNION_TYPE,
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
+				sheet: &book.Sheet{
+					Name: "ItemType",
+					Table: &book.Table{
+						MaxRow: 3,
+						MaxCol: 4,
+						Rows: [][]string{
+							{"Number", "Name", "Alias", "Field1"},
+							{"1", "PvpBattle", "SoloPVPBattle", "ID\nuint32"},
+							{"1", "PveBattle", "SoloPVEBattle", "Name\nstring"}, // duplicate
+						},
+					},
+				},
+			},
+			wantErr: true,
+			errcode: "E2022",
+		},
+		{
+			name: "MODE_UNION_TYPE dup name",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_UNION_TYPE,
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
+				sheet: &book.Sheet{
+					Name: "ItemType",
+					Table: &book.Table{
+						MaxRow: 3,
+						MaxCol: 3,
+						Rows: [][]string{
+							{"Name", "Alias", "Field1"},
+							{"Battle", "SoloPvpBattle", "ID\nuint32"},
+							{"Battle", "SoloPveBattle", "Name\nstring"}, // duplicate
+						},
+					},
+				},
+			},
+			wantErr: true,
+			errcode: "E2022",
+		},
+		{
+			name: "MODE_UNION_TYPE dup alias",
+			gen:  testgen,
+			args: args{
+				mode: tableaupb.Mode_MODE_UNION_TYPE,
+				ws:   &internalpb.Worksheet{Name: "ItemType"},
+				sheet: &book.Sheet{
+					Name: "ItemType",
+					Table: &book.Table{
+						MaxRow: 3,
+						MaxCol: 3,
+						Rows: [][]string{
+							{"Name", "Alias", "Field1"},
+							{"PvpBattle", "SoloBattle", "ID\nuint32"},
+							{"PveBattle", "SoloBattle", "Name\nstring"}, // duplicate
+						},
+					},
+				},
+			},
+			wantErr: true,
+			errcode: "E2022",
+		},
 		{
 			name: "MODE_UNION_TYPE_MULTI",
 			gen:  testgen,
@@ -790,8 +1082,15 @@ func TestGenerator_parseSpecialSheetMode(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if got, err := tt.gen.parseSpecialSheetMode(tt.args.mode, tt.args.ws, tt.args.sheet, "", ""); (err != nil) != tt.wantErr {
+			got, err := tt.gen.parseSpecialSheetMode(tt.args.mode, tt.args.ws, tt.args.sheet, "", "")
+			if (err != nil) != tt.wantErr {
 				t.Errorf("Generator.parseSpecialSheetMode() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if err != nil {
+				if tt.errcode != "" {
+					desc := xerrors.NewDesc(err)
+					require.Equal(t, tt.errcode, desc.ErrCode())
+				}
 			} else if len(got) != len(tt.want) {
 				t.Errorf("Generator.parseSpecialSheetMode() size = %v, want size %v", len(got), len(tt.want))
 			} else {