Files
opencloud/services/search/pkg/query/resolver.go
Dominik Schmidt cafd791fde feat(search): query open extension properties
KQL addresses an open extension property as extensions.<name>.<property>.
The literal picks the typed sibling: a number or date-time range asks the
number or date sibling, an equality asks every sibling the literal fits
(the lowercased string, and the number, boolean or date it also reads
as), = asks the string as written. Both compilers build the same plan.

The parity suite covers the queries and the lifecycle of extensions
through re-typing, removal, move and trash.
2026-09-16 09:20:40 +00:00

108 lines
3.2 KiB
Go

package query
import (
"reflect"
"strings"
"sync"
"github.com/opencloud-eu/opencloud/services/search/pkg/mapping"
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
)
// aliases are KQL spellings the derived index can't produce (fields are plural).
var aliases = map[string]string{
"tag": "Tags",
"favorite": "Favorites",
"driveid": "RootID",
}
// fieldIndex maps a lowercased KQL key to its canonical field name ("" is the
// bare-search default).
var fieldIndex = sync.OnceValue(func() map[string]string {
idx := mapping.FieldNameIndex(reflect.TypeFor[search.Resource](), search.Resource{}.SearchFieldOverrides())
for k, v := range aliases {
idx[k] = v
}
idx[""] = idx["name"]
return idx
})
// siblingFields lists which search siblings every field carries, from the
// resource struct and its overrides.
var siblingFields = sync.OnceValue(func() map[string]mapping.Siblings {
return mapping.SearchSiblings(reflect.TypeFor[search.Resource](), search.Resource{}.SearchFieldOverrides())
})
// pathFields are hierarchical path fields (TypePath), derived from the overrides.
var pathFields = sync.OnceValue(func() map[string]struct{} {
out := map[string]struct{}{}
for field, opts := range (search.Resource{}).SearchFieldOverrides() {
if opts.Type == mapping.TypePath {
out[field] = struct{}{}
}
}
return out
})
// fulltextFields are analyzed full-text fields (TypeFulltext), derived from the
// overrides.
var fulltextFields = sync.OnceValue(func() map[string]struct{} {
out := map[string]struct{}{}
for field, opts := range (search.Resource{}).SearchFieldOverrides() {
if opts.Type == mapping.TypeFulltext {
out[field] = struct{}{}
}
}
return out
})
// ResolveField maps a KQL key to its canonical field name; unknown keys pass through.
func ResolveField(name string) string {
if v, ok := fieldIndex()[strings.ToLower(name)]; ok {
return v
}
if mapping.IsOpenExtensionQueryField(name) {
return mapping.OpenExtensionsQueryPrefix + name[len(mapping.OpenExtensionsQueryPrefix):]
}
return name
}
// normalizedValueFields have their stored values normalized to lowercase at
// index time, so query values fold to match even though the fields themselves
// are case-preserved keywords.
var normalizedValueFields = map[string]struct{}{
"MimeType": {},
"Type": {},
"Hidden": {},
}
// FieldValueIsNormalized reports whether a field's stored values are
// normalized lowercase.
func FieldValueIsNormalized(field string) bool {
_, ok := normalizedValueFields[field]
return ok
}
// FieldIsCaseInsensitive reports whether a field's default search is case-insensitive.
func FieldIsCaseInsensitive(field string) bool {
return siblingFields()[field].Lowercase
}
// FieldIsPath reports whether a field is a hierarchical path field.
func FieldIsPath(field string) bool {
_, ok := pathFields()[field]
return ok
}
// FieldIsFulltext reports whether a field is an analyzed full-text field.
func FieldIsFulltext(field string) bool {
_, ok := fulltextFields()[field]
return ok
}
// FieldIsWordBroken reports whether a field is split into words, so a value
// without a wildcard matches it as a phrase of those words instead of as a whole.
func FieldIsWordBroken(field string) bool {
return siblingFields()[field].Words
}