Repository: tableauio/tableau (go)

Issue:
protoconf: support field presence for prop `unique`
## Problem

This problem is caused by PR #96.

Given a **vertical map** embedding an **incell list**, but want **incell list** to be configured in vertical layout :

| ID | Property|
|--|--|
| map<uint32, Item> | []int32 |
| Item's ID | Item's properties |
| 1 | 101 |
| 1 | 102 |
| 1 | 103 |
| 2 | 201 |
| 2 | 202 |

When generating config, it will raise `error[E2005]: Map contains multiple empty keys.`

## Solution

https://github.com/tableauio/tableau/blob/49ea8d784e05076e34377ae35a330c09e4262fba/proto/tableau/protobuf/tableau.proto#L165-L166

Add field presence for prop `unique`:
```
optional bool unique = 2; 
```


refer:
- https://github.com/protocolbuffers/protobuf/blob/main/docs/field_presence.md#go-example

Pull request description:
protoconf: support field presence for prop unique
- close #98

Patch:
diff --git a/.gitignore b/.gitignore
index 93a5aec..3ea26a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,5 @@ test/parallel/proto/
 test/parallel/protoconf/
 .vscode/
 *.prof
-test/functest/testdata/xml/*/*.csv
\ No newline at end of file
+test/functest/testdata/xml/*/*.csv
+coverage.txt
\ No newline at end of file
diff --git a/internal/confgen/parser.go b/internal/confgen/parser.go
index ea8ab50..0e0030c 100644
--- a/internal/confgen/parser.go
+++ b/internal/confgen/parser.go
@@ -431,8 +431,11 @@ func (sp *sheetParser) parseMapField(field *Field, msg protoreflect.Message, rc
 				return false, xerrors.WithMessageKV(err, rc.CellDebugKV(keyColName)...)
 			}
 			// check key uniqueness
-			if reflectMap.Has(newMapKey) && (prop.RequireUnique(field.opts.Prop) || sp.deduceMapKeyUnique(field, reflectMap)) {
-				return false, xerrors.WrapKV(xerrors.E2005(cell.Data), rc.CellDebugKV(keyColName)...)
+			if reflectMap.Has(newMapKey) {
+				if prop.RequireUnique(field.opts.Prop) ||
+					(!prop.HasUnique(field.opts.Prop) && sp.deduceMapKeyUnique(field, reflectMap)) {
+					return false, xerrors.WrapKV(xerrors.E2005(cell.Data), rc.CellDebugKV(keyColName)...)
+				}
 			}
 			if !keyPresent && !valuePresent {
 				// key and value are both not present.
@@ -551,8 +554,11 @@ func (sp *sheetParser) parseMapField(field *Field, msg protoreflect.Message, rc
 					continue
 				}
 				// check key uniqueness
-				if reflectMap.Has(newMapKey) && (prop.RequireUnique(field.opts.Prop) || sp.deduceMapKeyUnique(field, reflectMap)) {
-					return false, xerrors.WrapKV(xerrors.E2005(cell.Data), rc.CellDebugKV(keyColName)...)
+				if reflectMap.Has(newMapKey) {
+					if prop.RequireUnique(field.opts.Prop) ||
+						(!prop.HasUnique(field.opts.Prop) && sp.deduceMapKeyUnique(field, reflectMap)) {
+						return false, xerrors.WrapKV(xerrors.E2005(cell.Data), rc.CellDebugKV(keyColName)...)
+					}
 				}
 				reflectMap.Set(newMapKey, newMapValue)
 			}
diff --git a/internal/confgen/prop/prop.go b/internal/confgen/prop/prop.go
index 402de17..cf2fa07 100644
--- a/internal/confgen/prop/prop.go
+++ b/internal/confgen/prop/prop.go
@@ -11,9 +11,13 @@ import (
 	"google.golang.org/protobuf/reflect/protoreflect"
 )
 
+// HasUnique checks whether the unique field is set explicitly.
+func HasUnique(prop *tableaupb.FieldProp) bool {
+	return prop != nil && prop.Unique != nil
+}
+
 func RequireUnique(prop *tableaupb.FieldProp) bool {
-	unique := prop != nil && prop.Unique
-	return unique
+	return prop != nil && prop.Unique != nil && prop.GetUnique()
 }
 
 func CheckInRange(prop *tableaupb.FieldProp, fd protoreflect.FieldDescriptor, value protoreflect.Value, present bool) error {
diff --git a/proto/tableau/protobuf/tableau.proto b/proto/tableau/protobuf/tableau.proto
index be19908..0b8bd1a 100644
--- a/proto/tableau/protobuf/tableau.proto
+++ b/proto/tableau/protobuf/tableau.proto
@@ -163,7 +163,8 @@ message FieldProp {
   // Format: "1,10", "1,~", "~,10".
   string range = 1;
   // Whether the map key is unique in column.
-  bool unique = 2;
+  // If not set, then auto deduce whether the map key is unique or not.
+  optional bool unique = 2;
   // Ensure this field's value is in another sheet's
   // column value space (aka message's field value space).
   //
diff --git a/proto/tableaupb/tableau.pb.go b/proto/tableaupb/tableau.pb.go
index 5ed1864..21ba1cf 100644
--- a/proto/tableaupb/tableau.pb.go
+++ b/proto/tableaupb/tableau.pb.go
@@ -823,7 +823,8 @@ type FieldProp struct {
 	// Format: "1,10", "1,~", "~,10".
 	Range string `protobuf:"bytes,1,opt,name=range,proto3" json:"range,omitempty"`
 	// Whether the map key is unique in column.
-	Unique bool `protobuf:"varint,2,opt,name=unique,proto3" json:"unique,omitempty"`
+	// If not set, then auto deduce whether the map key is unique or not.
+	Unique *bool `protobuf:"varint,2,opt,name=unique,proto3,oneof" json:"unique,omitempty"`
 	// Ensure this field's value is in another sheet's
 	// column value space (aka message's field value space).
 	//
@@ -898,8 +899,8 @@ func (x *FieldProp) GetRange() string {
 }
 
 func (x *FieldProp) GetUnique() bool {
-	if x != nil {
-		return x.Unique
+	if x != nil && x.Unique != nil {
+		return *x.Unique
 	}
 	return false
 }
@@ -1126,24 +1127,25 @@ var file_tableau_protobuf_tableau_proto_rawDesc = []byte{
 	0x6e, 0x61, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6f, 0x70, 0x74, 0x69, 0x6f,
 	0x6e, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x04, 0x70, 0x72, 0x6f, 0x70, 0x18, 0x0f, 0x20, 0x01, 0x28,
 	0x0b, 0x32, 0x12, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x46, 0x69, 0x65, 0x6c,
-	0x64, 0x50, 0x72, 0x6f, 0x70, 0x52, 0x04, 0x70, 0x72, 0x6f, 0x70, 0x22, 0x9b, 0x02, 0x0a, 0x09,
+	0x64, 0x50, 0x72, 0x6f, 0x70, 0x52, 0x04, 0x70, 0x72, 0x6f, 0x70, 0x22, 0xab, 0x02, 0x0a, 0x09,
 	0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e,
 	0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12,
-	0x16, 0x0a, 0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
-	0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x66, 0x65, 0x72,
-	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x65, 0x66, 0x65, 0x72, 0x12, 0x1f, 0x0a,
-	0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x48,
-	0x00, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x88, 0x01, 0x01, 0x12, 0x18,
-	0x0a, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x78, 0x65,
-	0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x69, 0x78, 0x65, 0x64, 0x12, 0x12,
-	0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x69,
-	0x7a, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e,
-	0x32, 0x0d, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e, 0x46, 0x6f, 0x72, 0x6d, 0x52,
-	0x04, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1b, 0x0a, 0x09, 0x6a, 0x73, 0x6f, 0x6e, 0x5f, 0x6e, 0x61,
-	0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x73, 0x6f, 0x6e, 0x4e, 0x61,
-	0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x18, 0x0a, 0x20,
-	0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x42, 0x0b, 0x0a, 0x09,
+	0x1b, 0x0a, 0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48,
+	0x00, 0x52, 0x06, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05,
+	0x72, 0x65, 0x66, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x65, 0x66,
+	0x65, 0x72, 0x12, 0x1f, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04,
+	0x20, 0x01, 0x28, 0x03, 0x48, 0x01, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65,
+	0x88, 0x01, 0x01, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x18, 0x05,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x12, 0x14, 0x0a,
+	0x05, 0x66, 0x69, 0x78, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x69,
+	0x78, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28,
+	0x0d, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x66, 0x6f, 0x72, 0x6d, 0x18,
+	0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0d, 0x2e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x2e,
+	0x46, 0x6f, 0x72, 0x6d, 0x52, 0x04, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1b, 0x0a, 0x09, 0x6a, 0x73,
+	0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a,
+	0x73, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65,
+	0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e,
+	0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x42, 0x0b, 0x0a, 0x09,
 	0x5f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x2a, 0x5b, 0x0a, 0x06, 0x4c, 0x61, 0x79,
 	0x6f, 0x75, 0x74, 0x12, 0x12, 0x0a, 0x0e, 0x4c, 0x41, 0x59, 0x4f, 0x55, 0x54, 0x5f, 0x44, 0x45,
 	0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4c, 0x41, 0x59, 0x4f, 0x55,


Test patch:
diff --git a/internal/confgen/prop/prop_test.go b/internal/confgen/prop/prop_test.go
index f1d2755..709cd73 100644
--- a/internal/confgen/prop/prop_test.go
+++ b/internal/confgen/prop/prop_test.go
@@ -4,8 +4,53 @@ import (
 	"testing"
 
 	"github.com/tableauio/tableau/proto/tableaupb"
+	"google.golang.org/protobuf/proto"
 )
 
+func TestHasUnique(t *testing.T) {
+	type args struct {
+		prop *tableaupb.FieldProp
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "has-unique1",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Unique: proto.Bool(false),
+				},
+			},
+			want: true,
+		},
+		{
+			name: "has-unique2",
+			args: args{
+				prop: &tableaupb.FieldProp{
+					Unique: proto.Bool(true),
+				},
+			},
+			want: true,
+		},
+		{
+			name: "has-unique2",
+			args: args{
+				prop: &tableaupb.FieldProp{},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := HasUnique(tt.args.prop); got != tt.want {
+				t.Errorf("HasUnique() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
 func TestRequireUnique(t *testing.T) {
 	type args struct {
 		prop *tableaupb.FieldProp
@@ -19,7 +64,7 @@ func TestRequireUnique(t *testing.T) {
 			name: "require unique",
 			args: args{
 				prop: &tableaupb.FieldProp{
-					Unique: true,
+					Unique: proto.Bool(true),
 				},
 			},
 			want: true,
diff --git a/internal/protogen/exporter_test.go b/internal/protogen/exporter_test.go
index 7735ad7..68e145c 100644
--- a/internal/protogen/exporter_test.go
+++ b/internal/protogen/exporter_test.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/tableauio/tableau/proto/tableaupb"
+	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
 )
 
@@ -32,7 +33,7 @@ func Test_genFieldOptionsString(t *testing.T) {
 				opts: &tableaupb.FieldOptions{
 					Name: "ItemID",
 					Prop: &tableaupb.FieldProp{
-						Unique: true,
+						Unique: proto.Bool(true),
 					},
 				},
 			},
@@ -44,7 +45,7 @@ func Test_genFieldOptionsString(t *testing.T) {
 				opts: &tableaupb.FieldOptions{
 					Name: "ItemID",
 					Prop: &tableaupb.FieldProp{
-						Unique:   true,
+						Unique:   proto.Bool(true),
 						JsonName: "item_id_1",
 					},
 				},
@@ -202,7 +203,7 @@ func Test_marshalToText(t *testing.T) {
 					Key:    "ID",
 					Layout: tableaupb.Layout_LAYOUT_VERTICAL,
 					Prop: &tableaupb.FieldProp{
-						Unique:  true,
+						Unique:  proto.Bool(true),
 						Refer:   "ItemConf.ID",
 						Present: true,
 					},
diff --git a/internal/protogen/field_prop_test.go b/internal/protogen/field_prop_test.go
index 385ede4..0373ccc 100644
--- a/internal/protogen/field_prop_test.go
+++ b/internal/protogen/field_prop_test.go
@@ -69,13 +69,13 @@ func TestExtractMapFieldProp(t *testing.T) {
 			name: "noneEmptyMapFieldProp",
 			args: args{
 				prop: &tableaupb.FieldProp{
-					Unique:   true,
+					Unique:   proto.Bool(true),
 					Sequence: proto.Int64(1),
 					Size:     2,
 				},
 			},
 			want: &tableaupb.FieldProp{
-				Unique:   true,
+				Unique:   proto.Bool(true),
 				Sequence: proto.Int64(1),
 				Size:     2,
 			},
@@ -157,7 +157,7 @@ func TestExtractStructFieldProp(t *testing.T) {
 			name: "emptyStructFieldProp",
 			args: args{
 				prop: &tableaupb.FieldProp{
-					Unique: true,
+					Unique: proto.Bool(true),
 				},
 			},
 			want: nil,
@@ -166,7 +166,7 @@ func TestExtractStructFieldProp(t *testing.T) {
 			name: "noneEmptyStructFieldProp",
 			args: args{
 				prop: &tableaupb.FieldProp{
-					Unique: true,
+					Unique: proto.Bool(true),
 					Form:   tableaupb.Form_FORM_JSON,
 				},
 			},
@@ -197,7 +197,7 @@ func TestExtractScalarFieldProp(t *testing.T) {
 			name: "emptyScalarFieldProp",
 			args: args{
 				prop: &tableaupb.FieldProp{
-					Unique: true,
+					Unique: proto.Bool(true),
 				},
 			},
 			want: nil,
@@ -206,7 +206,7 @@ func TestExtractScalarFieldProp(t *testing.T) {
 			name: "noneEmptyStructFieldProp",
 			args: args{
 				prop: &tableaupb.FieldProp{
-					Unique: true,
+					Unique: proto.Bool(true),
 					Range:  "1~10",
 				},
 			},
diff --git a/test/functest/conf/EquipConf.json b/test/functest/conf/EquipConf.json
new file mode 100644
index 0000000..73bb4b2
--- /dev/null
+++ b/test/functest/conf/EquipConf.json
@@ -0,0 +1,16 @@
+{
+    "itemMap": {
+        "1000": {
+            "id": 1000,
+            "damage": 100
+        },
+        "1001": {
+            "id": 1001,
+            "damage": 200
+        },
+        "1002": {
+            "id": 1002,
+            "damage": 300
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/functest/conf/UniqueFalse.json b/test/functest/conf/UniqueFalse.json
new file mode 100644
index 0000000..929289b
--- /dev/null
+++ b/test/functest/conf/UniqueFalse.json
@@ -0,0 +1,25 @@
+{
+    "itemMap": {
+        "1": {
+            "id": 1,
+            "numList": [
+                101,
+                102,
+                103
+            ]
+        },
+        "2": {
+            "id": 2,
+            "numList": [
+                201,
+                202
+            ]
+        },
+        "3": {
+            "id": 3,
+            "numList": [
+                301
+            ]
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/functest/conf/VerticalPredefinedStructList.json b/test/functest/conf/VerticalPredefinedStructList.json
index 6c80026..8d931ee 100644
--- a/test/functest/conf/VerticalPredefinedStructList.json
+++ b/test/functest/conf/VerticalPredefinedStructList.json
@@ -1,5 +1,5 @@
 {
-    "itemList":  [
+    "itemList": [
         {
             "id": 1,
             "num": 100
diff --git a/test/functest/func_test.go b/test/functest/func_test.go
index 013f86f..36ff803 100644
--- a/test/functest/func_test.go
+++ b/test/functest/func_test.go
@@ -41,42 +41,44 @@ func Test_CompareGeneratedProto(t *testing.T) {
 		t.Errorf("%+v", err)
 		t.Fatalf("%s", xerrors.NewDesc(err))
 	}
-
 	oldConfDir := "proto"
 	newConfDir := "_proto"
 	files, err := os.ReadDir(oldConfDir)
-	if err != nil {
-		t.Fatalf("failed to read dir: %s", oldConfDir)
-	}
+	require.NoError(t, err)
 	for _, file := range files {
 		if !strings.HasSuffix(file.Name(), ".proto") {
 			continue
 		}
 		oldPath := filepath.Join(oldConfDir, file.Name())
 		absOldPath, err := filepath.Abs(oldPath)
-		require.ErrorIs(t, err, nil)
+		require.NoError(t, err)
 		oldfile, err := os.Open(oldPath)
-		if err != nil {
-			t.Fatal(err)
-		}
+		require.NoError(t, err)
+
 		newPath := filepath.Join(newConfDir, file.Name())
 		absNewPath, err := filepath.Abs(newPath)
-		require.ErrorIs(t, err, nil)
+		require.NoError(t, err)
 		newfile, err := os.Open(newPath)
-		if err != nil {
-			t.Fatal(err)
-		}
+		require.NoError(t, err)
 
-		sscan := bufio.NewScanner(oldfile)
-		dscan := bufio.NewScanner(newfile)
+		oscan := bufio.NewScanner(oldfile)
+		nscan := bufio.NewScanner(newfile)
 
-		for line := 1; sscan.Scan(); line++ {
-			dscan.Scan()
+		line := 0
+		for {
+			sok := oscan.Scan()
+			dok := nscan.Scan()
+			line++
+			require.Equalf(t, sok, dok, "unequal line count: %s:%d -> %s:%d", absOldPath, line, absNewPath, line)
+			if !sok || !dok {
+				break
+			}
 			if line == 1 {
-				// as the first line is one line comment (including dynamic version number), ignore it.
+				// as the first line is one line comment
+				// (including dynamic version number), ignore it.
 				continue
 			}
-			require.Equalf(t, string(sscan.Bytes()), string(dscan.Bytes()), "unequal: %s:%d -> %s:%d", absOldPath, line, absNewPath, line)
+			require.Equalf(t, string(oscan.Bytes()), string(nscan.Bytes()), "unequal line content: %s:%d -> %s:%d", absOldPath, line, absNewPath, line)
 		}
 	}
 }
@@ -91,31 +93,22 @@ func Test_CompareGeneratedJSON(t *testing.T) {
 	oldConfDir := "conf"
 	newConfDir := "_conf"
 	files, err := os.ReadDir(oldConfDir)
-	if err != nil {
-		t.Fatalf("failed to read dir: %s", oldConfDir)
-	}
+	require.NoError(t, err)
 	for _, file := range files {
 		if !strings.HasSuffix(file.Name(), ".json") {
 			continue
 		}
 		oldPath := filepath.Join(oldConfDir, file.Name())
 		absOldPath, err := filepath.Abs(oldPath)
-		if err != nil {
-			t.Fatal(err)
-		}
+		require.NoError(t, err)
 		newPath := filepath.Join(newConfDir, file.Name())
 		absNewPath, err := filepath.Abs(newPath)
-		if err != nil {
-			t.Fatal(err)
-		}
+		require.NoError(t, err)
 		oldData, err := os.ReadFile(oldPath)
-		if err != nil {
-			t.Fatal(err)
-		}
+		require.NoError(t, err)
 		newData, err := os.ReadFile(newPath)
-		if err != nil {
-			t.Fatal(err)
-		}
+		require.NoError(t, err)
+
 		t.Logf("compare json file: %s\n", file.Name())
 		require.JSONEqf(t, string(oldData), string(newData), "%s -> %s content not same.", absOldPath, absNewPath)
 	}
@@ -130,9 +123,7 @@ func Test_Excel2CSV(t *testing.T) {
 		}
 		return imp.ExportCSV()
 	})
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
+	require.NoError(t, err)
 }
 
 func Test_CSV2Excel(t *testing.T) {
@@ -144,7 +135,5 @@ func Test_CSV2Excel(t *testing.T) {
 		}
 		return imp.ExportExcel()
 	})
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
+	require.NoError(t, err)
 }
diff --git a/test/functest/proto/excel__fieldprop__field_prop.proto b/test/functest/proto/excel__fieldprop__field_prop.proto
index 61cbdf5..bd60de6 100644
--- a/test/functest/proto/excel__fieldprop__field_prop.proto
+++ b/test/functest/proto/excel__fieldprop__field_prop.proto
@@ -59,3 +59,13 @@ message ScalarFieldProp {
     uint32 optional_bonus_id = 6 [(tableau.field) = {name:"OptionalBonusID" prop:{refer:"ItemConf.ID"}}];
   }
 }
+
+message UniqueFalse {
+  option (tableau.worksheet) = {name:"UniqueFalse" namerow:1 typerow:2 noterow:3 datarow:4};
+
+  map<uint32, Item> item_map = 1 [(tableau.field) = {key:"ID" layout:LAYOUT_VERTICAL prop:{unique:false}}];
+  message Item {
+    uint32 id = 1 [(tableau.field) = {name:"ID"}];
+    repeated int32 num_list = 2 [(tableau.field) = {name:"Num" layout:LAYOUT_INCELL}];
+  }
+}
diff --git a/test/functest/proto/excel__keyedlist__keyed_list.proto b/test/functest/proto/excel__keyedlist__keyed_list.proto
index 94eed8f..5d54e50 100644
--- a/test/functest/proto/excel__keyedlist__keyed_list.proto
+++ b/test/functest/proto/excel__keyedlist__keyed_list.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__list__list.proto b/test/functest/proto/excel__list__list.proto
index f9ed358..ced6a53 100644
--- a/test/functest/proto/excel__list__list.proto
+++ b/test/functest/proto/excel__list__list.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__list__vertical_list.proto b/test/functest/proto/excel__list__vertical_list.proto
index e60e0bd..96b58ce 100644
--- a/test/functest/proto/excel__list__vertical_list.proto
+++ b/test/functest/proto/excel__list__vertical_list.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__map__horizontal_map.proto b/test/functest/proto/excel__map__horizontal_map.proto
index 3ef97bd..b1374b2 100644
--- a/test/functest/proto/excel__map__horizontal_map.proto
+++ b/test/functest/proto/excel__map__horizontal_map.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__map__map.proto b/test/functest/proto/excel__map__map.proto
index 06c17e9..5b62896 100644
--- a/test/functest/proto/excel__map__map.proto
+++ b/test/functest/proto/excel__map__map.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.3). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__map__vertical_map.proto b/test/functest/proto/excel__map__vertical_map.proto
index 168755d..8f72571 100644
--- a/test/functest/proto/excel__map__vertical_map.proto
+++ b/test/functest/proto/excel__map__vertical_map.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__metasheet__merger_1.proto b/test/functest/proto/excel__metasheet__merger_1.proto
index 2e4fa8f..a685158 100644
--- a/test/functest/proto/excel__metasheet__merger_1.proto
+++ b/test/functest/proto/excel__metasheet__merger_1.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.3). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__metasheet__metasheet.proto b/test/functest/proto/excel__metasheet__metasheet.proto
index acc43df..7efaee2 100644
--- a/test/functest/proto/excel__metasheet__metasheet.proto
+++ b/test/functest/proto/excel__metasheet__metasheet.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.3). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__metasheet__scatter_1.proto b/test/functest/proto/excel__metasheet__scatter_1.proto
index 7107650..8dffc6a 100644
--- a/test/functest/proto/excel__metasheet__scatter_1.proto
+++ b/test/functest/proto/excel__metasheet__scatter_1.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.3). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__misc__type_reuse.proto b/test/functest/proto/excel__misc__type_reuse.proto
index 63a0b2c..0a068aa 100644
--- a/test/functest/proto/excel__misc__type_reuse.proto
+++ b/test/functest/proto/excel__misc__type_reuse.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__nesting__list_in_list.proto b/test/functest/proto/excel__nesting__list_in_list.proto
index 5136779..207c47f 100644
--- a/test/functest/proto/excel__nesting__list_in_list.proto
+++ b/test/functest/proto/excel__nesting__list_in_list.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__nesting__list_in_map.proto b/test/functest/proto/excel__nesting__list_in_map.proto
index 2e48a21..9288a02 100644
--- a/test/functest/proto/excel__nesting__list_in_map.proto
+++ b/test/functest/proto/excel__nesting__list_in_map.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__nesting__map_in_list.proto b/test/functest/proto/excel__nesting__map_in_list.proto
index c638f23..4d14b7e 100644
--- a/test/functest/proto/excel__nesting__map_in_list.proto
+++ b/test/functest/proto/excel__nesting__map_in_list.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__nesting__map_in_map.proto b/test/functest/proto/excel__nesting__map_in_map.proto
index a6b58f7..4e2eb70 100644
--- a/test/functest/proto/excel__nesting__map_in_map.proto
+++ b/test/functest/proto/excel__nesting__map_in_map.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__nesting__struct_in_list.proto b/test/functest/proto/excel__nesting__struct_in_list.proto
index d0e301f..2be8df2 100644
--- a/test/functest/proto/excel__nesting__struct_in_list.proto
+++ b/test/functest/proto/excel__nesting__struct_in_list.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__nesting__struct_in_map.proto b/test/functest/proto/excel__nesting__struct_in_map.proto
index 1a4b69e..0383128 100644
--- a/test/functest/proto/excel__nesting__struct_in_map.proto
+++ b/test/functest/proto/excel__nesting__struct_in_map.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__nesting__struct_in_struct.proto b/test/functest/proto/excel__nesting__struct_in_struct.proto
index 43719a0..86677f7 100644
--- a/test/functest/proto/excel__nesting__struct_in_struct.proto
+++ b/test/functest/proto/excel__nesting__struct_in_struct.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__scalar__scalar.proto b/test/functest/proto/excel__scalar__scalar.proto
index 04031b4..d28669e 100644
--- a/test/functest/proto/excel__scalar__scalar.proto
+++ b/test/functest/proto/excel__scalar__scalar.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/excel__wellknown__well_known.proto b/test/functest/proto/excel__wellknown__well_known.proto
index bed6b9b..2e63e20 100644
--- a/test/functest/proto/excel__wellknown__well_known.proto
+++ b/test/functest/proto/excel__wellknown__well_known.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/xml__keyedlist__keyed_list.proto b/test/functest/proto/xml__keyedlist__keyed_list.proto
index e95a5b1..b7fe4ba 100644
--- a/test/functest/proto/xml__keyedlist__keyed_list.proto
+++ b/test/functest/proto/xml__keyedlist__keyed_list.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/xml__list__list.proto b/test/functest/proto/xml__list__list.proto
index 14329eb..8ed7e3c 100644
--- a/test/functest/proto/xml__list__list.proto
+++ b/test/functest/proto/xml__list__list.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.3). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/xml__map__map.proto b/test/functest/proto/xml__map__map.proto
index 5e6dc95..b1dc766 100644
--- a/test/functest/proto/xml__map__map.proto
+++ b/test/functest/proto/xml__map__map.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.3). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/proto/xml__metasheet__metasheet.proto b/test/functest/proto/xml__metasheet__metasheet.proto
index 7253e56..7b85f41 100644
--- a/test/functest/proto/xml__metasheet__metasheet.proto
+++ b/test/functest/proto/xml__metasheet__metasheet.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.4.2). DO NOT EDIT.
+// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
diff --git a/test/functest/testdata/excel/fieldprop/FieldProp#UniqueFalse.csv b/test/functest/testdata/excel/fieldprop/FieldProp#UniqueFalse.csv
new file mode 100644
index 0000000..29ff188
--- /dev/null
+++ b/test/functest/testdata/excel/fieldprop/FieldProp#UniqueFalse.csv
@@ -0,0 +1,9 @@
+ID,Num
+"map<uint32, Item>|{unique:false}",[]int32
+Item's ID,Item's num
+1,101
+1,102
+1,103
+2,201
+2,202
+3,301