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.
//go:generate mockgen -package $GOPACKAGE -source $GOFILE -destination mapper_mock.go
package searchattribute
import (
"errors"
"fmt"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/common/searchattribute/sadefs"
)
type (
// Mapper interface allows overriding custom search attribute names with aliases per namespace.
// Create an instance of a Mapper interface and pass it to the temporal.NewServer using temporal.WithSearchAttributesMapper.
// Returned error must be from the serviceerror package.
Mapper interface {
GetAlias(fieldName string, namespace string) (string, error)
GetFieldName(alias string, namespace string) (string, error)
}
// NoopMapper is an identity mapper that returns all field names and aliases unchanged.
NoopMapper struct{}
// This mapper preserves legacy custom search attribute behavior by falling back
// to identity mapping when the wrapped mapper misses but cluster metadata still
// recognizes the name as a legacy custom search attribute.
backCompMapper struct {
mapper Mapper
fallbackNameTypeMap NameTypeMap
}
MapperProvider interface {
GetMapper(nsName namespace.Name) (Mapper, error)
}
mapperProviderImpl struct {
customMapper Mapper
namespaceRegistry namespace.Registry
searchAttributesProvider Provider
fallbackIndexName string
}
)
var _ Mapper = (*NoopMapper)(nil)
var _ Mapper = (*backCompMapper)(nil)
var _ Mapper = (*namespace.CustomSearchAttributesMapper)(nil)
var _ MapperProvider = (*mapperProviderImpl)(nil)
return fieldName, nil
}
return alias, nil
}
func (m *backCompMapper) GetAlias(fieldName string, namespaceName string) (string, error) {
mapper.go ×1
alias, firstErr := m.mapper.GetAlias(fieldName, namespaceName)
if firstErr != nil {
return "", firstErr
}
// this is a custom search attribute registered through cluster metadata.
}
}
func (m *backCompMapper) GetFieldName(alias string, namespaceName string) (string, error) {
mapper.go ×1
fieldName, firstErr := m.mapper.GetFieldName(alias, namespaceName)
if firstErr != nil {
}
// this is a custom search attribute registered through cluster metadata.
}
}
_, err := m.fallbackNameTypeMap.getType(name, customCategory)
return err == nil
}
func NewMapperProvider(
customMapper Mapper,
namespaceRegistry namespace.Registry,
searchAttributesProvider Provider,
fallbackIndexName string,
return &mapperProviderImpl{
customMapper: customMapper,
namespaceRegistry: namespaceRegistry,
searchAttributesProvider: searchAttributesProvider,
fallbackIndexName: fallbackIndexName,
}
}
if m.customMapper != nil {
return m.customMapper, nil
}
if err != nil {
return nil, err
}
if m.fallbackIndexName != "" {
nameTypeMap, err := m.searchAttributesProvider.GetSearchAttributes(m.fallbackIndexName, false)
if err != nil {
return nil, fmt.Errorf("failed to load search attributes for fallback index %q: %w", m.fallbackIndexName, err)
mapper.go ×1
}
}
mapper: &saMapper,
fallbackNameTypeMap: fallbackNameTypeMap,
}, nil
}
legacyCustomSearchAttributes := make(map[string]enumspb.IndexedValueType)
for name, valueType := range nameTypeMap.Custom() {
if sadefs.IsPreallocatedCSAFieldName(name, valueType) {
}
}
}
// AliasFields returns SearchAttributes struct where each custom search attribute name is replaced with alias.
// If no replacement where made, it returns nil which means that original SearchAttributes struct should be used.
func AliasFields(
mapperProvider MapperProvider,
searchAttributes *commonpb.SearchAttributes,
namespaceName string,
mapper, err := mapperProvider.GetMapper(namespace.Name(namespaceName))
if err != nil {
return nil, err
}
}
newIndexedFields := make(map[string]*commonpb.Payload, len(searchAttributes.GetIndexedFields()))
mapper.go ×2
mapped := false
for saName, saPayload := range searchAttributes.GetIndexedFields() {
if !sadefs.IsMappable(saName) {
continue
}
if err != nil {
// Silently ignore serviceerror.InvalidArgument because it indicates unmapped field (alias was deleted, for example).
test_provider.go ×3
// IMPORTANT: AliasFields should never return serviceerror.InvalidArgument because it is used by Poll API and the error
// goes through up to SDK, which shutdowns worker when it receives serviceerror.InvalidArgument as poll response.
var invalidArgumentErr *serviceerror.InvalidArgument
if errors.As(err, &invalidArgumentErr) {
continue
}
return nil, err
}
mapped = true
}
newIndexedFields[aliasName] = saPayload
}
// If no field name was mapped, return nil to save on clone operation on caller side.
}
}
// UnaliasFields returns SearchAttributes struct where each search attribute alias is replaced with field name.
func UnaliasFields(
mapperProvider MapperProvider,
searchAttributes *commonpb.SearchAttributes,
namespaceName string,
mapper, err := mapperProvider.GetMapper(namespace.Name(namespaceName))
if err != nil {
return nil, err
}
}
newIndexedFields := make(map[string]*commonpb.Payload, len(searchAttributes.GetIndexedFields()))
mapper.go ×1
mapped := false
for saName, saPayload := range searchAttributes.GetIndexedFields() {
if !sadefs.IsMappable(saName) {
continue
}
if err != nil {
}
mapped = true
}
newIndexedFields[fieldName] = saPayload
}
}
}
// IsUserDefinedSearchAttribute returns true if alias refers to a user-defined custom search
// attribute rather than a synthetic one (e.g. the synthetic ScheduleId that maps to WorkflowId).
//
// Two independent checks are required because custom SAs can be registered in two ways:
// 1. Via UpdateNamespace with an explicit alias: stored in the Mapper as alias → field-name.
// GetFieldName returns the underlying field name (different from the alias), so the SA is
// identifiable even when it is absent from the NameTypeMap under the alias.
// 2. Via AddSearchAttributes without an alias: stored directly in NameTypeMap's custom map
// under the alias itself. GetFieldName returns an error (no mapping exists), so the type
// map is the only way to detect these.
func IsUserDefinedSearchAttribute(alias string, saMapper Mapper, saNameType NameTypeMap, ns string) bool {
schedule_id_query_rewriter.go ×6
// Check 1: explicit alias mapping resolves to a different underlying field name.
if mapped, err := saMapper.GetFieldName(alias, ns); err == nil && mapped != alias {
}
// Check 2: alias is registered as a custom SA in the type map (no alias mapping).
return ok
}