go.temporal.io/server/common/searchattribute/mapper.go

243 LOC · 113 covered · 130 uncovered · 48 ranges · 290 concepts · 33 introducers · 150 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 //go:generate mockgen -package $GOPACKAGE -source $GOFILE -destination mapper_mock.go
2
3 package searchattribute
4
5 import (
6 "errors"
7 "fmt"
8
9 commonpb "go.temporal.io/api/common/v1"
10 enumspb "go.temporal.io/api/enums/v1"
11 "go.temporal.io/api/serviceerror"
12 "go.temporal.io/server/common/namespace"
13 "go.temporal.io/server/common/searchattribute/sadefs"
14 )
15
16 type (
17 // Mapper interface allows overriding custom search attribute names with aliases per namespace.
18 // Create an instance of a Mapper interface and pass it to the temporal.NewServer using temporal.WithSearchAttributesMapper.
19 // Returned error must be from the serviceerror package.
20 Mapper interface {
21 GetAlias(fieldName string, namespace string) (string, error)
22 GetFieldName(alias string, namespace string) (string, error)
23 }
24
25 // NoopMapper is an identity mapper that returns all field names and aliases unchanged.
26 NoopMapper struct{}
27
28 // This mapper preserves legacy custom search attribute behavior by falling back
29 // to identity mapping when the wrapped mapper misses but cluster metadata still
30 // recognizes the name as a legacy custom search attribute.
31 backCompMapper struct {
32 mapper Mapper
33 fallbackNameTypeMap NameTypeMap
34 }
35
36 MapperProvider interface {
37 GetMapper(nsName namespace.Name) (Mapper, error)
38 }
39
40 mapperProviderImpl struct {
41 customMapper Mapper
42 namespaceRegistry namespace.Registry
43 searchAttributesProvider Provider
44 fallbackIndexName string
45 }
46 )
47
48 var _ Mapper = (*NoopMapper)(nil)
49 var _ Mapper = (*backCompMapper)(nil)
50 var _ Mapper = (*namespace.CustomSearchAttributesMapper)(nil)
51 var _ MapperProvider = (*mapperProviderImpl)(nil)
52
53 > func (*NoopMapper) GetAlias(fieldName string, _ string) (string, error) { mapper.go ×1
54 > return fieldName, nil
55 > }
56
57 > func (*NoopMapper) GetFieldName(alias string, _ string) (string, error) { mapper.go ×1
58 > return alias, nil
59 > }
60
61 > func (m *backCompMapper) GetAlias(fieldName string, namespaceName string) (string, error) { mapper.go ×1
62 > alias, firstErr := m.mapper.GetAlias(fieldName, namespaceName)
63 > if firstErr != nil {
64 > if !m.isLegacyCustomSearchAttribute(fieldName) { mapper.go ×3
65 return "", firstErr
66 }
67 // this is a custom search attribute registered through cluster metadata.
68 > return fieldName, nil mapper.go ×3
69 }
70 > return alias, nil visibility_store.go ×17
71 }
72
73 > func (m *backCompMapper) GetFieldName(alias string, namespaceName string) (string, error) { mapper.go ×1
74 > fieldName, firstErr := m.mapper.GetFieldName(alias, namespaceName)
75 > if firstErr != nil {
76 > if !m.isLegacyCustomSearchAttribute(alias) { mapper.go ×2
77 > return "", firstErr mapper.go ×1
78 > }
79 // this is a custom search attribute registered through cluster metadata.
80 > return alias, nil mapper.go ×3
81 }
82 > return fieldName, nil visibility_store.go ×17
83 }
84
85 > func (m *backCompMapper) isLegacyCustomSearchAttribute(name string) bool { mapper.go ×2
86 > _, err := m.fallbackNameTypeMap.getType(name, customCategory)
87 > return err == nil
88 > }
89
90 func NewMapperProvider(
91 customMapper Mapper,
92 namespaceRegistry namespace.Registry,
93 searchAttributesProvider Provider,
94 fallbackIndexName string,
95 > ) MapperProvider { mapper.go ×1
96 > return &mapperProviderImpl{
97 > customMapper: customMapper,
98 > namespaceRegistry: namespaceRegistry,
99 > searchAttributesProvider: searchAttributesProvider,
100 > fallbackIndexName: fallbackIndexName,
101 > }
102 > }
103
104 > func (m *mapperProviderImpl) GetMapper(nsName namespace.Name) (Mapper, error) { mapper.go ×3
105 > if m.customMapper != nil {
106 return m.customMapper, nil
107 }
108 > saMapper, err := m.namespaceRegistry.GetCustomSearchAttributesMapper(nsName) mapper.go ×3
109 > if err != nil {
110 return nil, err
111 }
112 > fallbackNameTypeMap := NameTypeMap{} mapper.go ×3
113 > if m.fallbackIndexName != "" {
114 > nameTypeMap, err := m.searchAttributesProvider.GetSearchAttributes(m.fallbackIndexName, false)
115 > if err != nil {
116 > return nil, fmt.Errorf("failed to load search attributes for fallback index %q: %w", m.fallbackIndexName, err) mapper.go ×1
117 > }
118 > fallbackNameTypeMap = legacyCustomSearchAttributes(nameTypeMap) mapper.go ×4
119 }
120 > return &backCompMapper{ mapper.go ×4
121 > mapper: &saMapper,
122 > fallbackNameTypeMap: fallbackNameTypeMap,
123 > }, nil
124 }
125
126 > func legacyCustomSearchAttributes(nameTypeMap NameTypeMap) NameTypeMap { mapper.go ×4
127 > legacyCustomSearchAttributes := make(map[string]enumspb.IndexedValueType)
128 > for name, valueType := range nameTypeMap.Custom() {
129 > if sadefs.IsPreallocatedCSAFieldName(name, valueType) {
130 > continue mapper.go ×1
131 }
132 > legacyCustomSearchAttributes[name] = valueType namespace.go ×1
133 }
134 > return NewNameTypeMap(legacyCustomSearchAttributes) mapper.go ×4
135 }
136
137 // AliasFields returns SearchAttributes struct where each custom search attribute name is replaced with alias.
138 // If no replacement where made, it returns nil which means that original SearchAttributes struct should be used.
139 func AliasFields(
140 mapperProvider MapperProvider,
141 searchAttributes *commonpb.SearchAttributes,
142 namespaceName string,
143 > ) (*commonpb.SearchAttributes, error) { mapper.go ×2
144 > mapper, err := mapperProvider.GetMapper(namespace.Name(namespaceName))
145 > if err != nil {
146 return nil, err
147 }
148
149 > if len(searchAttributes.GetIndexedFields()) == 0 || mapper == nil { mapper.go ×2
150 > return searchAttributes, nil mapper.go ×1
151 > }
152
153 > newIndexedFields := make(map[string]*commonpb.Payload, len(searchAttributes.GetIndexedFields())) mapper.go ×2
154 > mapped := false
155 > for saName, saPayload := range searchAttributes.GetIndexedFields() {
156 > if !sadefs.IsMappable(saName) {
157 > newIndexedFields[saName] = saPayload mapper.go ×1
158 > continue
159 }
160
161 > aliasName, err := mapper.GetAlias(saName, namespaceName) mapper.go ×3
162 > if err != nil {
163 > // Silently ignore serviceerror.InvalidArgument because it indicates unmapped field (alias was deleted, for example). test_provider.go ×3
164 > // IMPORTANT: AliasFields should never return serviceerror.InvalidArgument because it is used by Poll API and the error
165 > // goes through up to SDK, which shutdowns worker when it receives serviceerror.InvalidArgument as poll response.
166 > var invalidArgumentErr *serviceerror.InvalidArgument
167 > if errors.As(err, &invalidArgumentErr) {
168 > continue
169 }
170 return nil, err
171 }
172 > if aliasName != saName { mapper.go ×3
173 > mapped = true
174 > }
175 > newIndexedFields[aliasName] = saPayload
176 }
177
178 // If no field name was mapped, return nil to save on clone operation on caller side.
179 > if !mapped { mapper.go ×2
180 > return searchAttributes, nil mapper.go ×1
181 > }
182 > return &commonpb.SearchAttributes{IndexedFields: newIndexedFields}, nil mapper.go ×3
183 }
184
185 // UnaliasFields returns SearchAttributes struct where each search attribute alias is replaced with field name.
186 func UnaliasFields(
187 mapperProvider MapperProvider,
188 searchAttributes *commonpb.SearchAttributes,
189 namespaceName string,
190 > ) (*commonpb.SearchAttributes, error) { mapper.go ×2
191 > mapper, err := mapperProvider.GetMapper(namespace.Name(namespaceName))
192 > if err != nil {
193 return nil, err
194 }
195
196 > if len(searchAttributes.GetIndexedFields()) == 0 || mapper == nil { mapper.go ×2
197 > return searchAttributes, nil mapper.go ×1
198 > }
199
200 > newIndexedFields := make(map[string]*commonpb.Payload, len(searchAttributes.GetIndexedFields())) mapper.go ×1
201 > mapped := false
202 > for saName, saPayload := range searchAttributes.GetIndexedFields() {
203 > if !sadefs.IsMappable(saName) {
204 > newIndexedFields[saName] = saPayload telemetry.go ×2
205 > continue
206 }
207
208 > fieldName, err := mapper.GetFieldName(saName, namespaceName) mapper.go ×1
209 > if err != nil {
210 > return nil, err mapper.go ×1
211 > }
212 > if fieldName != saName { mapper.go ×2
213 > mapped = true
214 > }
215 > newIndexedFields[fieldName] = saPayload
216 }
217
218 > if !mapped { mapper.go ×1
219 > return searchAttributes, nil mapper.go ×1
220 > }
221
222 > return &commonpb.SearchAttributes{IndexedFields: newIndexedFields}, nil mapper.go ×2
223 }
224
225 // IsUserDefinedSearchAttribute returns true if alias refers to a user-defined custom search
226 // attribute rather than a synthetic one (e.g. the synthetic ScheduleId that maps to WorkflowId).
227 //
228 // Two independent checks are required because custom SAs can be registered in two ways:
229 // 1. Via UpdateNamespace with an explicit alias: stored in the Mapper as alias → field-name.
230 // GetFieldName returns the underlying field name (different from the alias), so the SA is
231 // identifiable even when it is absent from the NameTypeMap under the alias.
232 // 2. Via AddSearchAttributes without an alias: stored directly in NameTypeMap's custom map
233 // under the alias itself. GetFieldName returns an error (no mapping exists), so the type
234 // map is the only way to detect these.
235 > func IsUserDefinedSearchAttribute(alias string, saMapper Mapper, saNameType NameTypeMap, ns string) bool { schedule_id_query_rewriter.go ×6
236 > // Check 1: explicit alias mapping resolves to a different underlying field name.
237 > if mapped, err := saMapper.GetFieldName(alias, ns); err == nil && mapped != alias {
238 > return true mapper.go ×1
239 > }
240 // Check 2: alias is registered as a custom SA in the type map (no alias mapping).
241 > _, ok := saNameType.Custom()[alias] mapper.go ×1
242 > return ok
243 }