From bd91586ed56f62556996581596d184fcf8c16296 Mon Sep 17 00:00:00 2001 From: Thomas Timmers Date: Wed, 4 Nov 2026 10:24:53 +0100 Subject: [PATCH 1/2] feat(datacomparison): reference a column by tag DEV-231852 Add EntityTypeColumnReferenceByTag with optional TagKey and TagValue fields on Reference. Validate requires TagKey for the new type. Co-Authored-By: Claude Sonnet --- datacomparison/entitytype_enumer.go | 12 +++-- datacomparison/reference.go | 11 +++++ datacomparison/reference_test.go | 76 ++++++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/datacomparison/entitytype_enumer.go b/datacomparison/entitytype_enumer.go index dcbc48f..e86898e 100644 --- a/datacomparison/entitytype_enumer.go +++ b/datacomparison/entitytype_enumer.go @@ -10,11 +10,11 @@ import ( "strings" ) -const _EntityTypeName = "DataObjectColumnReferenceByName" +const _EntityTypeName = "DataObjectColumnReferenceByNameColumnReferenceByTag" -var _EntityTypeIndex = [...]uint8{0, 10, 31} +var _EntityTypeIndex = [...]uint8{0, 10, 31, 51} -const _EntityTypeLowerName = "dataobjectcolumnreferencebyname" +const _EntityTypeLowerName = "dataobjectcolumnreferencebynamecolumnreferencebytag" func (i EntityType) String() string { if i < 0 || i >= EntityType(len(_EntityTypeIndex)-1) { @@ -33,20 +33,24 @@ func _EntityTypeNoOp() { var x [1]struct{} _ = x[EntityTypeDataObject-(0)] _ = x[EntityTypeColumnReferenceByName-(1)] + _ = x[EntityTypeColumnReferenceByTag-(2)] } -var _EntityTypeValues = []EntityType{EntityTypeDataObject, EntityTypeColumnReferenceByName} +var _EntityTypeValues = []EntityType{EntityTypeDataObject, EntityTypeColumnReferenceByName, EntityTypeColumnReferenceByTag} var _EntityTypeNameToValueMap = map[string]EntityType{ _EntityTypeName[0:10]: EntityTypeDataObject, _EntityTypeLowerName[0:10]: EntityTypeDataObject, _EntityTypeName[10:31]: EntityTypeColumnReferenceByName, _EntityTypeLowerName[10:31]: EntityTypeColumnReferenceByName, + _EntityTypeName[31:51]: EntityTypeColumnReferenceByTag, + _EntityTypeLowerName[31:51]: EntityTypeColumnReferenceByTag, } var _EntityTypeNames = []string{ _EntityTypeName[0:10], _EntityTypeName[10:31], + _EntityTypeName[31:51], } // EntityTypeString retrieves an enum value from the enum constants string name. diff --git a/datacomparison/reference.go b/datacomparison/reference.go index 771da9f..d914a20 100644 --- a/datacomparison/reference.go +++ b/datacomparison/reference.go @@ -2,6 +2,7 @@ package datacomparison import ( "context" + "errors" "fmt" "github.com/collibra/go-bexpression/base" @@ -13,14 +14,24 @@ type EntityType int const ( EntityTypeDataObject EntityType = iota EntityTypeColumnReferenceByName + EntityTypeColumnReferenceByTag ) +var ErrTagKeyRequired = errors.New("tagKey is required for a ColumnReferenceByTag reference") + type Reference struct { EntityType EntityType `json:"entityType,omitempty" yaml:"entityType,omitempty" gqlgen:"entityType"` EntityID string `json:"entityId,omitempty" yaml:"entityId,omitempty" gqlgen:"entityId"` + // TagKey and TagValue select the column for EntityTypeColumnReferenceByTag. An empty TagValue matches any value. + TagKey string `json:"tagKey,omitempty" yaml:"tagKey,omitempty" gqlgen:"tagKey"` + TagValue string `json:"tagValue,omitempty" yaml:"tagValue,omitempty" gqlgen:"tagValue"` } func (r *Reference) Validate(_ context.Context) error { + if r.EntityType == EntityTypeColumnReferenceByTag && r.TagKey == "" { + return ErrTagKeyRequired + } + return nil } diff --git a/datacomparison/reference_test.go b/datacomparison/reference_test.go index fbe6d60..2f85e60 100644 --- a/datacomparison/reference_test.go +++ b/datacomparison/reference_test.go @@ -2,6 +2,7 @@ package datacomparison import ( "context" + "encoding/json" "errors" "testing" @@ -72,7 +73,78 @@ func TestReference_Accept(t *testing.T) { } func TestReference_Validate(t *testing.T) { - r := Reference{} + tests := []struct { + name string + ref Reference + wantErr error + }{ + {name: "empty reference", ref: Reference{}}, + {name: "column reference by name", ref: Reference{EntityType: EntityTypeColumnReferenceByName, EntityID: "COUNTRY"}}, + {name: "tag reference with key", ref: Reference{EntityType: EntityTypeColumnReferenceByTag, TagKey: "Country"}}, + {name: "tag reference with key and value", ref: Reference{EntityType: EntityTypeColumnReferenceByTag, TagKey: "Country", TagValue: "DE"}}, + {name: "tag reference without key", ref: Reference{EntityType: EntityTypeColumnReferenceByTag}, wantErr: ErrTagKeyRequired}, + {name: "tag reference with value only", ref: Reference{EntityType: EntityTypeColumnReferenceByTag, TagValue: "DE"}, wantErr: ErrTagKeyRequired}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.ref.Validate(context.Background()) + + if tt.wantErr == nil { + assert.NoError(t, err) + return + } + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestReference_JSONRoundTrip(t *testing.T) { + tests := []struct { + name string + ref Reference + wantJSON string + }{ + { + name: "tag key and value", + ref: Reference{EntityType: EntityTypeColumnReferenceByTag, TagKey: "Country", TagValue: "DE"}, + wantJSON: `{"entityType":"ColumnReferenceByTag","tagKey":"Country","tagValue":"DE"}`, + }, + { + name: "tag key only omits the value", + ref: Reference{EntityType: EntityTypeColumnReferenceByTag, TagKey: "Country"}, + wantJSON: `{"entityType":"ColumnReferenceByTag","tagKey":"Country"}`, + }, + { + name: "column reference by name omits the tag fields", + ref: Reference{EntityType: EntityTypeColumnReferenceByName, EntityID: "COUNTRY"}, + wantJSON: `{"entityType":"ColumnReferenceByName","entityId":"COUNTRY"}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := json.Marshal(tt.ref) + assert.NoError(t, err) + assert.JSONEq(t, tt.wantJSON, string(got)) + + var back Reference + assert.NoError(t, json.Unmarshal(got, &back)) + assert.Equal(t, tt.ref, back) + }) + } +} + +func TestEntityType_ColumnReferenceByTag(t *testing.T) { + assert.Equal(t, "ColumnReferenceByTag", EntityTypeColumnReferenceByTag.String()) + + got, err := EntityTypeString("ColumnReferenceByTag") + assert.NoError(t, err) + assert.Equal(t, EntityTypeColumnReferenceByTag, got) +} + +func TestReference_Accept_TagReference(t *testing.T) { + ref := &Reference{EntityType: EntityTypeColumnReferenceByTag, TagKey: "Country", TagValue: "DE"} + visitorMock := base.NewMockVisitor(t) + visitorMock.EXPECT().Literal(mock.Anything, ref).Return(nil).Once() - assert.NoError(t, r.Validate(context.Background())) + assert.NoError(t, ref.Accept(context.Background(), visitorMock)) } From 7a8f1089ded3dd4d18ddc7499238f575cece144e Mon Sep 17 00:00:00 2001 From: Thomas Timmers Date: Thu, 5 Nov 2026 14:18:13 +0100 Subject: [PATCH 2/2] fix(datacomparison): address review findings DEV-231852 Document the tag reference in the language grammar, add a YAML round-trip test and fold the tag Accept test into the existing table. gopkg.in/yaml.v3 becomes a direct dependency for the test. Co-Authored-By: Claude Sonnet --- datacomparison/reference_test.go | 63 ++++++++++++++++++++++++++++---- go.mod | 6 ++- language | 2 +- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/datacomparison/reference_test.go b/datacomparison/reference_test.go index 2f85e60..c9eff48 100644 --- a/datacomparison/reference_test.go +++ b/datacomparison/reference_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "gopkg.in/yaml.v3" "github.com/collibra/go-bexpression/base" ) @@ -16,6 +17,8 @@ func TestReference_Accept(t *testing.T) { type fields struct { EntityType EntityType EntityID string + TagKey string + TagValue string } type args struct { ctx context.Context @@ -41,6 +44,21 @@ func TestReference_Accept(t *testing.T) { }, wantErr: false, }, + { + name: "tag reference", + fields: fields{ + EntityType: EntityTypeColumnReferenceByTag, + TagKey: "Country", + TagValue: "DE", + }, + args: args{ + ctx: context.Background(), + visitorSetup: func(visitor *base.MockVisitor) { + visitor.EXPECT().Literal(mock.Anything, &Reference{EntityType: EntityTypeColumnReferenceByTag, TagKey: "Country", TagValue: "DE"}).Return(nil).Once() + }, + }, + wantErr: false, + }, { name: "visitor failed", fields: fields{ @@ -64,6 +82,8 @@ func TestReference_Accept(t *testing.T) { r := &Reference{ EntityType: tt.fields.EntityType, EntityID: tt.fields.EntityID, + TagKey: tt.fields.TagKey, + TagValue: tt.fields.TagValue, } if err := r.Accept(tt.args.ctx, visitorMock); (err != nil) != tt.wantErr { t.Errorf("Accept() error = %v, wantErr %v", err, tt.wantErr) @@ -133,6 +153,41 @@ func TestReference_JSONRoundTrip(t *testing.T) { } } +func TestReference_YAMLRoundTrip(t *testing.T) { + tests := []struct { + name string + ref Reference + wantYAML string + }{ + { + name: "tag key and value", + ref: Reference{EntityType: EntityTypeColumnReferenceByTag, TagKey: "Country", TagValue: "DE"}, + wantYAML: "entityType: ColumnReferenceByTag\ntagKey: Country\ntagValue: DE\n", + }, + { + name: "tag key only omits the value", + ref: Reference{EntityType: EntityTypeColumnReferenceByTag, TagKey: "Country"}, + wantYAML: "entityType: ColumnReferenceByTag\ntagKey: Country\n", + }, + { + name: "column reference by name omits the tag fields", + ref: Reference{EntityType: EntityTypeColumnReferenceByName, EntityID: "COUNTRY"}, + wantYAML: "entityType: ColumnReferenceByName\nentityId: COUNTRY\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := yaml.Marshal(tt.ref) + assert.NoError(t, err) + assert.Equal(t, tt.wantYAML, string(got)) + + var back Reference + assert.NoError(t, yaml.Unmarshal(got, &back)) + assert.Equal(t, tt.ref, back) + }) + } +} + func TestEntityType_ColumnReferenceByTag(t *testing.T) { assert.Equal(t, "ColumnReferenceByTag", EntityTypeColumnReferenceByTag.String()) @@ -140,11 +195,3 @@ func TestEntityType_ColumnReferenceByTag(t *testing.T) { assert.NoError(t, err) assert.Equal(t, EntityTypeColumnReferenceByTag, got) } - -func TestReference_Accept_TagReference(t *testing.T) { - ref := &Reference{EntityType: EntityTypeColumnReferenceByTag, TagKey: "Country", TagValue: "DE"} - visitorMock := base.NewMockVisitor(t) - visitorMock.EXPECT().Literal(mock.Anything, ref).Return(nil).Once() - - assert.NoError(t, ref.Accept(context.Background(), visitorMock)) -} diff --git a/go.mod b/go.mod index 8f1cbdb..97efc7e 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,10 @@ tool ( github.com/vektra/mockery/v3 ) -require github.com/stretchr/testify v1.11.1 +require ( + github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 +) require ( github.com/brunoga/deep v1.2.5 // indirect @@ -50,5 +53,4 @@ require ( golang.org/x/term v0.35.0 // indirect golang.org/x/text v0.29.0 // indirect golang.org/x/tools v0.37.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/language b/language index 0a618a0..e900018 100644 --- a/language +++ b/language @@ -11,7 +11,7 @@ BinaryOperand: | Reference: - ENTITYTYPE ENTITYID + ENTITYTYPE ENTITYID | ColumnReferenceByTag TAGKEY [TAGVALUE] Literal: BOOL|INT|FLOAT|STRING|TIMESTAMP