Skip to content

Commit 1322d4b

Browse files
committed
CHASM Visibility support for Text type search attribute
1 parent 1314043 commit 1322d4b

5 files changed

Lines changed: 128 additions & 26 deletions

File tree

chasm/search_attribute.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ var (
5757
SearchAttributeFieldKeywordList01 = newSearchAttributeFieldKeywordList(1)
5858
SearchAttributeFieldKeywordList02 = newSearchAttributeFieldKeywordList(2)
5959

60+
// CHASM search attribute of type Text is not supported at this moment.
61+
// SearchAttributeFieldText01 = newSearchAttributeFieldText(1)
62+
63+
// Predefined search attributes don't have alias.
64+
// TaskQueue is a system search attribute outside CHASM, but treated as predefined inside CHASM.
6065
SearchAttributeTaskQueue = newSearchAttributeKeywordByField(sadefs.TaskQueue)
6166
SearchAttributeTemporalChangeVersion = newSearchAttributeKeywordListByField(sadefs.TemporalChangeVersion)
6267
SearchAttributeBinaryChecksums = newSearchAttributeKeywordListByField(sadefs.BinaryChecksums)
@@ -76,21 +81,21 @@ var (
7681
)
7782

7883
var (
79-
// CHASM search attribute of type Text is not supported at this moment.
80-
// Note that it's currently assumed that string type values are Keyword search attributes.
8184
_ SearchAttribute = (*SearchAttributeBool)(nil)
8285
_ SearchAttribute = (*SearchAttributeDateTime)(nil)
8386
_ SearchAttribute = (*SearchAttributeInt)(nil)
8487
_ SearchAttribute = (*SearchAttributeDouble)(nil)
8588
_ SearchAttribute = (*SearchAttributeKeyword)(nil)
8689
_ SearchAttribute = (*SearchAttributeKeywordList)(nil)
90+
_ SearchAttribute = (*SearchAttributeText)(nil)
8791

8892
_ typedSearchAttribute[bool] = (*SearchAttributeBool)(nil)
8993
_ typedSearchAttribute[time.Time] = (*SearchAttributeDateTime)(nil)
9094
_ typedSearchAttribute[int64] = (*SearchAttributeInt)(nil)
9195
_ typedSearchAttribute[float64] = (*SearchAttributeDouble)(nil)
9296
_ typedSearchAttribute[string] = (*SearchAttributeKeyword)(nil)
9397
_ typedSearchAttribute[[]string] = (*SearchAttributeKeywordList)(nil)
98+
_ typedSearchAttribute[string] = (*SearchAttributeText)(nil)
9499
)
95100

96101
type (
@@ -194,6 +199,17 @@ func newSearchAttributeFieldKeywordList(index int) SearchAttributeFieldKeywordLi
194199
}
195200
}
196201

202+
// SearchAttributeFieldText is a search attribute field for a text value.
203+
type SearchAttributeFieldText struct {
204+
field string
205+
}
206+
207+
func newSearchAttributeFieldText(index int) SearchAttributeFieldText {
208+
return SearchAttributeFieldText{
209+
field: resolveFieldName(enumspb.INDEXED_VALUE_TYPE_TEXT, index),
210+
}
211+
}
212+
197213
func resolveFieldName(valueType enumspb.IndexedValueType, index int) string {
198214
// Columns are named like TemporalBool01, TemporalDatetime01, TemporalDouble01, TemporalInt01.
199215
return fmt.Sprintf("%s%s%02d", sadefs.ReservedPrefix, valueType.String(), index)
@@ -415,6 +431,43 @@ func (s SearchAttributeKeywordList) Value(value []string) SearchAttributeKeyValu
415431

416432
func (s SearchAttributeKeywordList) typeMarker(_ []string) {}
417433

434+
// SearchAttributeText is a search attribute for a text value.
435+
type SearchAttributeText struct {
436+
searchAttributeDefinition
437+
}
438+
439+
// NewSearchAttributeText creates a new text search attribute given a predefined chasm field
440+
func NewSearchAttributeText(alias string, textField SearchAttributeFieldText) SearchAttributeText {
441+
return SearchAttributeText{
442+
searchAttributeDefinition: searchAttributeDefinition{
443+
alias: alias,
444+
field: textField.field,
445+
valueType: enumspb.INDEXED_VALUE_TYPE_TEXT,
446+
},
447+
}
448+
}
449+
450+
func newSearchAttributeTextByField(field string) SearchAttributeText {
451+
return SearchAttributeText{
452+
searchAttributeDefinition: searchAttributeDefinition{
453+
alias: field,
454+
field: field,
455+
valueType: enumspb.INDEXED_VALUE_TYPE_TEXT,
456+
},
457+
}
458+
}
459+
460+
// Value sets the string value of the search attribute.
461+
func (s SearchAttributeText) Value(value string) SearchAttributeKeyValue {
462+
return SearchAttributeKeyValue{
463+
Alias: s.alias,
464+
Field: s.field,
465+
Value: VisibilityValueText(value),
466+
}
467+
}
468+
469+
func (s SearchAttributeText) typeMarker(_ string) {}
470+
418471
// SearchAttributesMap wraps search attribute values with type-safe access.
419472
type SearchAttributesMap struct {
420473
values map[string]VisibilityValue
@@ -438,7 +491,7 @@ func newSearchAttributesMapFromProto(
438491
for saName, saPayload := range searchAttributes.IndexedFields {
439492
value, err := visibilityValueFromPayload(saPayload)
440493
if err != nil {
441-
return SearchAttributesMap{}, nil
494+
return SearchAttributesMap{}, err
442495
}
443496
result.values[saName] = value
444497
}

chasm/search_attribute_test.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"go.temporal.io/server/common/searchattribute/sadefs"
1111
)
1212

13+
var textField01 = newSearchAttributeFieldText(1)
14+
1315
func TestSearchAttributesMap_Get(t *testing.T) {
1416
// Define test search attributes
1517
boolAttr := NewSearchAttributeBool("completed", SearchAttributeFieldBool01)
@@ -18,6 +20,7 @@ func TestSearchAttributesMap_Get(t *testing.T) {
1820
keywordAttr := NewSearchAttributeKeyword("status", SearchAttributeFieldKeyword01)
1921
datetimeAttr := NewSearchAttributeDateTime("timestamp", SearchAttributeFieldDateTime01)
2022
keywordListAttr := NewSearchAttributeKeywordList("tags", SearchAttributeFieldKeywordList01)
23+
textAttr := NewSearchAttributeText("summary", textField01)
2124

2225
now := time.Now()
2326

@@ -29,6 +32,7 @@ func TestSearchAttributesMap_Get(t *testing.T) {
2932
"status": VisibilityValueKeyword("active"),
3033
"timestamp": VisibilityValueTime(now),
3134
"tags": VisibilityValueStringSlice([]string{"tag1", "tag2"}),
35+
"summary": VisibilityValueText("foo bar"),
3236
}
3337
m := NewSearchAttributesMap(values)
3438

@@ -68,6 +72,12 @@ func TestSearchAttributesMap_Get(t *testing.T) {
6872
require.Equal(t, []string{"tag1", "tag2"}, val)
6973
})
7074

75+
t.Run("GetText", func(t *testing.T) {
76+
val, ok := SearchAttributeValue(m, textAttr)
77+
require.True(t, ok)
78+
require.Equal(t, "foo bar", val)
79+
})
80+
7181
t.Run("NotFound", func(t *testing.T) {
7282
missingAttr := NewSearchAttributeBool("missing", SearchAttributeFieldBool02)
7383
val, ok := SearchAttributeValue(m, missingAttr)
@@ -123,6 +133,7 @@ func TestNewSearchAttributesMapFromProto(t *testing.T) {
123133
"status": sadefs.MustEncodeValue("active", enumspb.INDEXED_VALUE_TYPE_KEYWORD),
124134
"timestamp": sadefs.MustEncodeValue(now, enumspb.INDEXED_VALUE_TYPE_DATETIME),
125135
"tags": sadefs.MustEncodeValue([]string{"tag1", "tag2"}, enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST),
136+
"summary": sadefs.MustEncodeValue("foo bar", enumspb.INDEXED_VALUE_TYPE_TEXT),
126137
},
127138
}
128139
m, err := newSearchAttributesMapFromProto(sa)
@@ -157,6 +168,12 @@ func TestNewSearchAttributesMapFromProto(t *testing.T) {
157168
listVal, ok := SearchAttributeValue(m, keywordListAttr)
158169
require.True(t, ok)
159170
require.Equal(t, []string{"tag1", "tag2"}, listVal)
171+
172+
textAttr := NewSearchAttributeText("summary", textField01)
173+
textVal, ok := SearchAttributeValue(m, textAttr)
174+
require.True(t, ok)
175+
require.Equal(t, "foo bar", textVal)
176+
160177
})
161178

162179
t.Run("InvalidPayload", func(t *testing.T) {
@@ -165,9 +182,7 @@ func TestNewSearchAttributesMapFromProto(t *testing.T) {
165182
"bad": {Data: []byte("not valid")},
166183
},
167184
}
168-
m, err := newSearchAttributesMapFromProto(sa)
169-
// Current implementation returns nil error on decode failure
170-
require.NoError(t, err)
171-
require.Empty(t, m.values)
185+
_, err := newSearchAttributesMapFromProto(sa)
186+
require.Error(t, err)
172187
})
173188
}

chasm/visibility.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ func (v *VisibilitySearchAttributesMapper) Alias(field string) (string, error) {
6161
}
6262
alias, ok := v.fieldToAlias[field]
6363
if !ok {
64-
return "", serviceerror.NewInvalidArgument(fmt.Sprintf("visibility search attributes mapper has no registered field %q", field))
64+
return "", serviceerror.NewInvalidArgumentf(
65+
"visibility search attributes mapper has no registered field %q",
66+
field,
67+
)
6568
}
6669
return alias, nil
6770
}

chasm/visibility_value.go

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -134,32 +134,48 @@ func isVisibilityValueEqual(v1, v2 VisibilityValue) bool {
134134
return v1.Equal(v2)
135135
}
136136

137+
type VisibilityValueText string
138+
139+
func (v VisibilityValueText) MustEncode() *commonpb.Payload {
140+
return sadefs.MustEncodeValue(string(v), enumspb.INDEXED_VALUE_TYPE_TEXT)
141+
}
142+
143+
func (v VisibilityValueText) Equal(other VisibilityValue) bool {
144+
ov, ok := other.(VisibilityValueText)
145+
if !ok {
146+
return false
147+
}
148+
return v == ov
149+
}
150+
151+
func (v VisibilityValueText) Value() any {
152+
return string(v)
153+
}
154+
137155
// visibilityValueFromPayload decoded payload based on type set in its metadata.
138156
func visibilityValueFromPayload(payload *commonpb.Payload) (VisibilityValue, error) {
139157
value, err := sadefs.DecodeValue(payload, enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED, false)
140158
if err != nil {
141159
return nil, err
142160
}
143161

144-
switch val := value.(type) {
145-
case int64:
146-
return VisibilityValueInt64(val), nil
147-
case float64:
148-
return VisibilityValueFloat64(val), nil
149-
case bool:
150-
return VisibilityValueBool(val), nil
151-
case time.Time:
152-
return VisibilityValueTime(val), nil
153-
case string:
154-
// Try to parse as datetime first
155-
if parsedTime, err := time.Parse(time.RFC3339, val); err == nil {
156-
return VisibilityValueTime(parsedTime), nil
157-
}
158-
return VisibilityValueKeyword(val), nil
159-
case []string:
160-
return VisibilityValueStringSlice(val), nil
162+
switch t := sadefs.GetMetadataType(payload); t {
163+
case enumspb.INDEXED_VALUE_TYPE_BOOL:
164+
return VisibilityValueBool(value.(bool)), nil
165+
case enumspb.INDEXED_VALUE_TYPE_DATETIME:
166+
return VisibilityValueTime(value.(time.Time)), nil
167+
case enumspb.INDEXED_VALUE_TYPE_DOUBLE:
168+
return VisibilityValueFloat64(value.(float64)), nil
169+
case enumspb.INDEXED_VALUE_TYPE_INT:
170+
return VisibilityValueInt64(value.(int64)), nil
171+
case enumspb.INDEXED_VALUE_TYPE_KEYWORD:
172+
return VisibilityValueKeyword(value.(string)), nil
173+
case enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST:
174+
return VisibilityValueStringSlice(value.([]string)), nil
175+
case enumspb.INDEXED_VALUE_TYPE_TEXT:
176+
return VisibilityValueText(value.(string)), nil
161177
default:
162178
// this should never happen given that DecodeValue did not return an error
163-
return nil, fmt.Errorf("unexpected search attribute value type %T", value)
179+
return nil, fmt.Errorf("unexpected metadata type %s", t)
164180
}
165181
}

chasm/visibility_value_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,21 @@ func TestVisibilityValue(t *testing.T) {
100100
require.False(t, v.Equal(VisibilityValueTime(base.Add(time.Second))))
101101
require.False(t, v.Equal(VisibilityValueKeyword(base.String())))
102102
})
103+
104+
t.Run("Text", func(t *testing.T) {
105+
v := VisibilityValueText("foo bar")
106+
p := v.MustEncode()
107+
require.NotNil(t, p)
108+
109+
var out string
110+
err := payload.Decode(p, &out)
111+
require.NoError(t, err)
112+
require.Equal(t, "foo bar", out)
113+
114+
require.True(t, v.Equal(VisibilityValueText("foo bar")))
115+
require.False(t, v.Equal(VisibilityValueText("foo")))
116+
require.False(t, v.Equal(VisibilityValueBool(true)))
117+
})
103118
}
104119

105120
func TestIsVisibilityValueEqual(t *testing.T) {

0 commit comments

Comments
 (0)