Skip to content

Commit 7eb764d

Browse files
authored
fix: pre-flight validation for update_description on unsupported entity types (#189)
validateEntityTypeForChange was not checking descriptionSupportedTypes for entity-level update_description, allowing unsupported types (e.g. tag) to pass pre-flight and fail at DataHub write time — breaking batch atomicity. Also preserves error chain in wrapUnsupportedEntityTypeError, uses entityTypeDataset constant consistently, and replaces append-prepend with slices.Insert.
1 parent bbecbe4 commit 7eb764d

File tree

2 files changed

+36
-11
lines changed

2 files changed

+36
-11
lines changed

pkg/toolkits/knowledge/entity_type.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ package knowledge
33
import (
44
"errors"
55
"fmt"
6+
"slices"
67
"strings"
78

89
dhclient "github.com/txn2/mcp-datahub/pkg/client"
910
)
1011

11-
// entityTypeDataset is the DataHub entity type string for datasets.
12-
const entityTypeDataset = "dataset"
12+
const (
13+
// entityTypeDataset is the DataHub entity type string for datasets.
14+
entityTypeDataset = "dataset"
15+
16+
// opsSeparator is the delimiter used when joining supported operations in error messages.
17+
opsSeparator = ", "
18+
)
1319

1420
// entityTypeFromURN extracts the entity type from a DataHub URN.
1521
// For example, "urn:li:dataset:(...)" returns "dataset".
@@ -37,7 +43,7 @@ func supportedOpsForType(entityType string) []string {
3743
}
3844

3945
if descriptionSupportedTypes[entityType] {
40-
ops = append([]string{"update_description"}, ops...)
46+
ops = slices.Insert(ops, 0, "update_description")
4147
}
4248

4349
if entityType == entityTypeDataset {
@@ -48,7 +54,7 @@ func supportedOpsForType(entityType string) []string {
4854
}
4955

5056
// descriptionSupportedTypes are entity types that support update_description.
51-
// This matches the upstream mcp-datahub descriptionAspectMap.
57+
// Must stay in sync with the upstream mcp-datahub descriptionAspectMap.
5258
var descriptionSupportedTypes = map[string]bool{
5359
"dataset": true,
5460
"dashboard": true,
@@ -73,23 +79,32 @@ func validateEntityTypeForChange(urn string, c ApplyChange) error {
7379
// Column-level descriptions are dataset-only (schema metadata is a dataset concept).
7480
if c.ChangeType == string(actionUpdateDescription) {
7581
if _, isColumn := parseColumnTarget(c.Target); isColumn {
76-
if entityType != "dataset" {
82+
if entityType != entityTypeDataset {
7783
return fmt.Errorf(
7884
"column-level update_description is only supported for datasets, not %s entities. "+
7985
"Supported operations for %s: %s",
80-
entityType, entityType, strings.Join(supportedOpsForType(entityType), ", "),
86+
entityType, entityType, strings.Join(supportedOpsForType(entityType), opsSeparator),
8187
)
8288
}
8389
return nil
8490
}
91+
92+
// Entity-level update_description is only supported for specific entity types.
93+
if !descriptionSupportedTypes[entityType] {
94+
return fmt.Errorf(
95+
"update_description is not supported for %s entities. "+
96+
"Supported operations for %s: %s",
97+
entityType, entityType, strings.Join(supportedOpsForType(entityType), opsSeparator),
98+
)
99+
}
85100
}
86101

87102
// Dataset-only operations.
88-
if datasetOnlyOperations[actionType(c.ChangeType)] && entityType != "dataset" {
103+
if datasetOnlyOperations[actionType(c.ChangeType)] && entityType != entityTypeDataset {
89104
return fmt.Errorf(
90105
"%s is only supported for datasets, not %s entities. "+
91106
"Supported operations for %s: %s",
92-
c.ChangeType, entityType, entityType, strings.Join(supportedOpsForType(entityType), ", "),
107+
c.ChangeType, entityType, entityType, strings.Join(supportedOpsForType(entityType), opsSeparator),
93108
)
94109
}
95110

@@ -114,8 +129,8 @@ func wrapUnsupportedEntityTypeError(err error, urn string) error {
114129

115130
return fmt.Errorf(
116131
"update_description is not supported for %s entities. "+
117-
"Supported operations for %s: %s",
118-
entityType, entityType, strings.Join(supportedOpsForType(entityType), ", "),
132+
"Supported operations for %s: %s: %w",
133+
entityType, entityType, strings.Join(supportedOpsForType(entityType), opsSeparator), err,
119134
)
120135
}
121136

pkg/toolkits/knowledge/entity_type_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ func TestValidateEntityTypeForChange(t *testing.T) {
106106
change: ApplyChange{ChangeType: "update_description", Detail: "desc"},
107107
},
108108

109+
// update_description on unsupported entity types
110+
{
111+
name: "update_description on tag (unsupported)",
112+
urn: tagURN,
113+
change: ApplyChange{ChangeType: "update_description", Detail: "desc"},
114+
wantErr: true,
115+
errContain: "update_description is not supported for tag entities",
116+
},
117+
109118
// Column-level descriptions: dataset only
110119
{
111120
name: "column description on dataset",
@@ -288,8 +297,9 @@ func TestWrapUnsupportedEntityTypeError(t *testing.T) {
288297
require.Error(t, got)
289298
assert.Contains(t, got.Error(), "update_description is not supported for tag entities")
290299
assert.Contains(t, got.Error(), "Supported operations for tag")
291-
// Should NOT be the same error object (wrapped)
300+
// Should NOT be the same error object but must preserve the error chain.
292301
assert.NotEqual(t, orig, got)
302+
assert.True(t, errors.Is(got, dhclient.ErrUnsupportedEntityType), "wrapped error should preserve error chain")
293303
})
294304

295305
t.Run("wrapped ErrUnsupportedEntityType", func(t *testing.T) {

0 commit comments

Comments
 (0)