From 37f2cc1ef9bfcc7971f0bb07db955646d70ee705 Mon Sep 17 00:00:00 2001 From: Tristan Swadell Date: Wed, 2 Nov 2022 16:07:03 -0700 Subject: [PATCH] Optional field selection runtime (#599) * Refactor of attributes qualification to support presence testing * Implementation of optional field selection runtime changes * CEL library changes to expose runtime functions --- cel/BUILD.bazel | 1 + cel/library.go | 33 +- interpreter/attribute_patterns.go | 2 +- interpreter/attribute_patterns_test.go | 4 +- interpreter/attributes.go | 270 ++++++++++++--- interpreter/attributes_test.go | 458 ++++++++++++++++++++++--- interpreter/interpretable.go | 23 +- interpreter/planner.go | 67 ++-- 8 files changed, 711 insertions(+), 147 deletions(-) diff --git a/cel/BUILD.bazel b/cel/BUILD.bazel index e973abfc..ddddbd28 100644 --- a/cel/BUILD.bazel +++ b/cel/BUILD.bazel @@ -23,6 +23,7 @@ go_library( "//checker/decls:go_default_library", "//common:go_default_library", "//common/containers:go_default_library", + "//common/operators:go_default_library", "//common/overloads:go_default_library", "//common/types:go_default_library", "//common/types/pb:go_default_library", diff --git a/cel/library.go b/cel/library.go index 9ff0ec3b..8d1648ab 100644 --- a/cel/library.go +++ b/cel/library.go @@ -20,6 +20,7 @@ import ( "time" "github.com/google/cel-go/checker" + "github.com/google/cel-go/common/operators" "github.com/google/cel-go/common/overloads" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" @@ -87,19 +88,22 @@ func (stdLibrary) ProgramOptions() []ProgramOption { type optionalLibrary struct{} func (optionalLibrary) CompileOptions() []EnvOption { - paramType := TypeParamType("T") - optionalType := OpaqueType("optional", paramType) + paramTypeK := TypeParamType("K") + paramTypeV := TypeParamType("V") + optionalTypeV := OptionalType(paramTypeV) + listTypeV := ListType(paramTypeV) + mapTypeKV := MapType(paramTypeK, paramTypeV) return []EnvOption{ Types(types.OptionalType), // Global and member functions for working with optional values. Function("optional.of", - Overload("optional_of", []*Type{paramType}, optionalType, + Overload("optional_of", []*Type{paramTypeV}, optionalTypeV, UnaryBinding(func(value ref.Val) ref.Val { return types.OptionalOf(value) }))), Function("optional.ofNonZeroValue", - Overload("optional_ofNonZeroValue", []*Type{paramType}, optionalType, + Overload("optional_ofNonZeroValue", []*Type{paramTypeV}, optionalTypeV, UnaryBinding(func(value ref.Val) ref.Val { v, isZeroer := value.(traits.Zeroer) if !isZeroer || !v.IsZeroValue() { @@ -108,18 +112,18 @@ func (optionalLibrary) CompileOptions() []EnvOption { return types.OptionalNone }))), Function("optional.none", - Overload("optional_none", []*Type{}, optionalType, + Overload("optional_none", []*Type{}, optionalTypeV, FunctionBinding(func(values ...ref.Val) ref.Val { return types.OptionalNone }))), Function("value", - MemberOverload("optional_value", []*Type{optionalType}, paramType, + MemberOverload("optional_value", []*Type{optionalTypeV}, paramTypeV, UnaryBinding(func(value ref.Val) ref.Val { opt := value.(*types.Optional) return opt.GetValue() }))), Function("hasValue", - MemberOverload("optional_hasValue", []*Type{optionalType}, paramType, + MemberOverload("optional_hasValue", []*Type{optionalTypeV}, paramTypeV, UnaryBinding(func(value ref.Val) ref.Val { opt := value.(*types.Optional) return types.Bool(opt.HasValue()) @@ -127,9 +131,20 @@ func (optionalLibrary) CompileOptions() []EnvOption { // Implementation of 'or' and 'orValue' are special-cased to support short-circuiting in the // evaluation chain. Function("or", - MemberOverload("optional_or_optional", []*Type{optionalType, optionalType}, optionalType)), + MemberOverload("optional_or_optional", []*Type{optionalTypeV, optionalTypeV}, optionalTypeV)), Function("orValue", - MemberOverload("optional_orValue_value", []*Type{optionalType, paramType}, paramType)), + MemberOverload("optional_orValue_value", []*Type{optionalTypeV, paramTypeV}, paramTypeV)), + // OptSelect is handled specially by the type-checker, so the receiver's field type is used to determine the + // optput type. + Function(operators.OptSelect, + MemberOverload("select_optional_field", []*Type{DynType, StringType}, optionalTypeV)), + // OptIndex is handled mostly like any other indexing operation on a list or map, so the type-checker can use + // these signatures to determine type-agreement without any special handling. + Function(operators.OptIndex, + MemberOverload("list_index_optional_int", []*Type{listTypeV, IntType}, optionalTypeV), + MemberOverload("optional_list_index_optional_int", []*Type{OptionalType(listTypeV), IntType}, optionalTypeV), + MemberOverload("map_index_optional_value", []*Type{mapTypeKV, paramTypeK}, optionalTypeV), + MemberOverload("optional_map_index_optional_value", []*Type{OptionalType(mapTypeKV), paramTypeK}, optionalTypeV)), } } diff --git a/interpreter/attribute_patterns.go b/interpreter/attribute_patterns.go index 4ff4fc93..afb7c8d5 100644 --- a/interpreter/attribute_patterns.go +++ b/interpreter/attribute_patterns.go @@ -272,7 +272,7 @@ func (fac *partialAttributeFactory) matchesUnknownPatterns( } // If this resolution behavior ever changes, new implementations of the // qualifierValueEquator may be required to handle proper resolution. - qual, err = fac.NewQualifier(nil, qual.ID(), val) + qual, err = fac.NewQualifier(nil, qual.ID(), val, attr.IsOptional()) if err != nil { return nil, err } diff --git a/interpreter/attribute_patterns_test.go b/interpreter/attribute_patterns_test.go index 10bb849d..718385d9 100644 --- a/interpreter/attribute_patterns_test.go +++ b/interpreter/attribute_patterns_test.go @@ -303,7 +303,7 @@ func TestAttributePattern_CrossReference(t *testing.T) { map[string]any{"a": []int64{1, 2}, "b": 0}, NewAttributePattern("a").QualInt(0).QualString("c")) // Qualify a[b] with 'c', a[b].c - c, _ := fac.NewQualifier(nil, 3, "c") + c, _ := fac.NewQualifier(nil, 3, "c", false) a.AddQualifier(c) // The resolve step should return unknown val, err = a.Resolve(partVars) @@ -324,7 +324,7 @@ func genAttr(fac AttributeFactory, a attr) Attribute { attr = fac.AbsoluteAttribute(1, a.name) } for _, q := range a.quals { - qual, _ := fac.NewQualifier(nil, id, q) + qual, _ := fac.NewQualifier(nil, id, q, false) attr.AddQualifier(qual) id++ } diff --git a/interpreter/attributes.go b/interpreter/attributes.go index 9391db5b..468fdae7 100644 --- a/interpreter/attributes.go +++ b/interpreter/attributes.go @@ -61,7 +61,7 @@ type AttributeFactory interface { // The qualifier may consider the object type being qualified, if present. If absent, the // qualification should be considered dynamic and the qualification should still work, though // it may be sub-optimal. - NewQualifier(objType *exprpb.Type, qualID int64, val any) (Qualifier, error) + NewQualifier(objType *exprpb.Type, qualID int64, val any, opt bool) (Qualifier, error) } // Qualifier marker interface for designating different qualifier values and where they appear @@ -70,6 +70,12 @@ type Qualifier interface { // ID where the qualifier appears within an expression. ID() int64 + // IsOptional specifies whether the qualifier is optional. + // Instead of a direct qualification, an optional qualifier will be resolved via QualifyIfPresent + // rather than Qualify. A non-optional qualifier may also be resolved through QualifyIfPresent if + // the object to qualify is itself optional. + IsOptional() bool + // Qualify performs a qualification, e.g. field selection, on the input object and returns // the value of the access and whether the value was set. A non-nil value with a false presence // test result indicates that the value being returned is the default value. @@ -88,6 +94,7 @@ type Qualifier interface { type ConstantQualifier interface { Qualifier + // Value returns the constant value associated with the qualifier. Value() ref.Val } @@ -192,7 +199,7 @@ func (r *attrFactory) RelativeAttribute(id int64, operand Interpretable) Attribu } // NewQualifier is an implementation of the AttributeFactory interface. -func (r *attrFactory) NewQualifier(objType *exprpb.Type, qualID int64, val any) (Qualifier, error) { +func (r *attrFactory) NewQualifier(objType *exprpb.Type, qualID int64, val any, opt bool) (Qualifier, error) { // Before creating a new qualifier check to see if this is a protobuf message field access. // If so, use the precomputed GetFrom qualification method rather than the standard // stringQualifier. @@ -205,10 +212,11 @@ func (r *attrFactory) NewQualifier(objType *exprpb.Type, qualID int64, val any) Name: str, FieldType: ft, adapter: r.adapter, + optional: opt, }, nil } } - return newQualifier(r.adapter, qualID, val) + return newQualifier(r.adapter, qualID, val, opt) } type absoluteAttribute struct { @@ -227,6 +235,13 @@ func (a *absoluteAttribute) ID() int64 { return a.id } +// IsOptional returns trivially false for an attribute as the attribute represents a fully +// qualified variable name. If the attribute is used in an optional manner, then an attrQualifier +// is created and marks the attribute as optional. +func (a *absoluteAttribute) IsOptional() bool { + return false +} + // Cost implements the Coster interface method. func (a *absoluteAttribute) Cost() (min, max int64) { for _, q := range a.qualifiers { @@ -281,15 +296,13 @@ func (a *absoluteAttribute) Resolve(vars Activation) (any, error) { // If the variable is found, process it. Otherwise, wait until the checks to // determine whether the type is unknown before returning. obj, found := vars.ResolveName(nm) - var qualObj any - var err error if found { - for _, qual := range a.qualifiers { - qualObj, err = qual.Qualify(vars, obj) - if err != nil { - return nil, err - } - obj = qualObj + obj, isOpt, err := applyQualifiers(vars, obj, a.qualifiers) + if err != nil { + return nil, err + } + if isOpt { + return types.OptionalOf(a.adapter.NativeToValue(obj)), nil } return obj, nil } @@ -322,6 +335,13 @@ func (a *conditionalAttribute) ID() int64 { return a.id } +// IsOptional returns trivially false for an attribute as the attribute represents a fully +// qualified variable name. If the attribute is used in an optional manner, then an attrQualifier +// is created and marks the attribute as optional. +func (a *conditionalAttribute) IsOptional() bool { + return false +} + // Cost provides the heuristic cost of a ternary operation ? : . // The cost is computed as cost(expr) plus the min/max costs of evaluating either // `t` or `f`. @@ -359,9 +379,6 @@ func (a *conditionalAttribute) QualifyIfPresent(vars Activation, obj any, presen // Resolve evaluates the condition, and then resolves the truthy or falsy branch accordingly. func (a *conditionalAttribute) Resolve(vars Activation) (any, error) { val := a.expr.Eval(vars) - if types.IsError(val) { - return nil, val.(*types.Err) - } if val == types.True { return a.truthy.Resolve(vars) } @@ -392,6 +409,13 @@ func (a *maybeAttribute) ID() int64 { return a.id } +// IsOptional returns trivially false for an attribute as the attribute represents a fully +// qualified variable name. If the attribute is used in an optional manner, then an attrQualifier +// is created and marks the attribute as optional. +func (a *maybeAttribute) IsOptional() bool { + return false +} + // Cost implements the Coster interface method. The min cost is computed as the minimal cost among // all the possible attributes, the max cost ditto. func (a *maybeAttribute) Cost() (min, max int64) { @@ -404,20 +428,6 @@ func (a *maybeAttribute) Cost() (min, max int64) { return } -func findMin(x, y int64) int64 { - if x < y { - return x - } - return y -} - -func findMax(x, y int64) int64 { - if x > y { - return x - } - return y -} - // AddQualifier adds a qualifier to each possible attribute variant, and also creates // a new namespaced variable from the qualified value. // @@ -526,6 +536,13 @@ func (a *relativeAttribute) ID() int64 { return a.id } +// IsOptional returns trivially false for an attribute as the attribute represents a fully +// qualified variable name. If the attribute is used in an optional manner, then an attrQualifier +// is created and marks the attribute as optional. +func (a *relativeAttribute) IsOptional() bool { + return false +} + // Cost implements the Coster interface method. func (a *relativeAttribute) Cost() (min, max int64) { min, max = estimateCost(a.operand) @@ -563,14 +580,12 @@ func (a *relativeAttribute) Resolve(vars Activation) (any, error) { if types.IsUnknown(v) { return v, nil } - // Next, qualify it. Qualification handles unknowns as well, so there's no need to recheck. - var obj any = v - for _, qual := range a.qualifiers { - qualObj, err := qual.Qualify(vars, obj) - if err != nil { - return nil, err - } - obj = qualObj + obj, isOpt, err := applyQualifiers(vars, v, a.qualifiers) + if err != nil { + return nil, err + } + if isOpt { + return types.OptionalOf(a.adapter.NativeToValue(obj)), nil } return obj, nil } @@ -580,44 +595,93 @@ func (a *relativeAttribute) String() string { return fmt.Sprintf("id: %v, operand: %v", a.id, a.operand) } -func newQualifier(adapter ref.TypeAdapter, id int64, v any) (Qualifier, error) { +func newQualifier(adapter ref.TypeAdapter, id int64, v any, opt bool) (Qualifier, error) { var qual Qualifier switch val := v.(type) { case Attribute: - return &attrQualifier{id: id, Attribute: val}, nil + // Note, attributes are initially identified as non-optional since they represent a top-level + // field access; however, when used as a relative qualifier, e.g. a[?b.c], then an attrQualifier + // is created which intercepts the IsOptional check for the attribute in order to return the + // correct result. + return &attrQualifier{ + id: id, + Attribute: val, + optional: opt, + }, nil case string: - qual = &stringQualifier{id: id, value: val, celValue: types.String(val), adapter: adapter} + qual = &stringQualifier{ + id: id, + value: val, + celValue: types.String(val), + adapter: adapter, + optional: opt, + } case int: - qual = &intQualifier{id: id, value: int64(val), celValue: types.Int(val), adapter: adapter} + qual = &intQualifier{ + id: id, value: int64(val), celValue: types.Int(val), adapter: adapter, optional: opt, + } case int32: - qual = &intQualifier{id: id, value: int64(val), celValue: types.Int(val), adapter: adapter} + qual = &intQualifier{ + id: id, value: int64(val), celValue: types.Int(val), adapter: adapter, optional: opt, + } case int64: - qual = &intQualifier{id: id, value: val, celValue: types.Int(val), adapter: adapter} + qual = &intQualifier{ + id: id, value: val, celValue: types.Int(val), adapter: adapter, optional: opt, + } case uint: - qual = &uintQualifier{id: id, value: uint64(val), celValue: types.Uint(val), adapter: adapter} + qual = &uintQualifier{ + id: id, value: uint64(val), celValue: types.Uint(val), adapter: adapter, optional: opt, + } case uint32: - qual = &uintQualifier{id: id, value: uint64(val), celValue: types.Uint(val), adapter: adapter} + qual = &uintQualifier{ + id: id, value: uint64(val), celValue: types.Uint(val), adapter: adapter, optional: opt, + } case uint64: - qual = &uintQualifier{id: id, value: val, celValue: types.Uint(val), adapter: adapter} + qual = &uintQualifier{ + id: id, value: val, celValue: types.Uint(val), adapter: adapter, optional: opt, + } case bool: - qual = &boolQualifier{id: id, value: val, celValue: types.Bool(val), adapter: adapter} + qual = &boolQualifier{ + id: id, value: val, celValue: types.Bool(val), adapter: adapter, optional: opt, + } case float32: - qual = &doubleQualifier{id: id, value: float64(val), celValue: types.Double(val), adapter: adapter} + qual = &doubleQualifier{ + id: id, + value: float64(val), + celValue: types.Double(val), + adapter: adapter, + optional: opt, + } case float64: - qual = &doubleQualifier{id: id, value: val, celValue: types.Double(val), adapter: adapter} + qual = &doubleQualifier{ + id: id, value: val, celValue: types.Double(val), adapter: adapter, optional: opt, + } case types.String: - qual = &stringQualifier{id: id, value: string(val), celValue: val, adapter: adapter} + qual = &stringQualifier{ + id: id, value: string(val), celValue: val, adapter: adapter, optional: opt, + } case types.Int: - qual = &intQualifier{id: id, value: int64(val), celValue: val, adapter: adapter} + qual = &intQualifier{ + id: id, value: int64(val), celValue: val, adapter: adapter, optional: opt, + } case types.Uint: - qual = &uintQualifier{id: id, value: uint64(val), celValue: val, adapter: adapter} + qual = &uintQualifier{ + id: id, value: uint64(val), celValue: val, adapter: adapter, optional: opt, + } case types.Bool: - qual = &boolQualifier{id: id, value: bool(val), celValue: val, adapter: adapter} + qual = &boolQualifier{ + id: id, value: bool(val), celValue: val, adapter: adapter, optional: opt, + } case types.Double: - qual = &doubleQualifier{id: id, value: float64(val), celValue: val, adapter: adapter} + qual = &doubleQualifier{ + id: id, value: float64(val), celValue: val, adapter: adapter, optional: opt, + } case types.Unknown: qual = &unknownQualifier{id: id, value: val} default: + if q, ok := v.(Qualifier); ok { + return q, nil + } return nil, fmt.Errorf("invalid qualifier type: %T", v) } return qual, nil @@ -626,12 +690,20 @@ func newQualifier(adapter ref.TypeAdapter, id int64, v any) (Qualifier, error) { type attrQualifier struct { id int64 Attribute + optional bool } +// ID implements the Qualifier interface method and returns the qualification instruction id +// rather than the attribute id. func (q *attrQualifier) ID() int64 { return q.id } +// IsOptional implements the Qualifier interface method. +func (q *attrQualifier) IsOptional() bool { + return q.optional +} + // Cost returns zero for constant field qualifiers func (q *attrQualifier) Cost() (min, max int64) { return estimateCost(q.Attribute) @@ -642,6 +714,7 @@ type stringQualifier struct { value string celValue ref.Val adapter ref.TypeAdapter + optional bool } // ID is an implementation of the Qualifier interface method. @@ -649,6 +722,11 @@ func (q *stringQualifier) ID() int64 { return q.id } +// IsOptional implements the Qualifier interface method. +func (q *stringQualifier) IsOptional() bool { + return q.optional +} + // Qualify implements the Qualifier interface method. func (q *stringQualifier) Qualify(vars Activation, obj any) (any, error) { val, _, err := q.qualifyInternal(vars, obj, false, false) @@ -742,6 +820,7 @@ type intQualifier struct { value int64 celValue ref.Val adapter ref.TypeAdapter + optional bool } // ID is an implementation of the Qualifier interface method. @@ -749,6 +828,11 @@ func (q *intQualifier) ID() int64 { return q.id } +// IsOptional implements the Qualifier interface method. +func (q *intQualifier) IsOptional() bool { + return q.optional +} + // Qualify implements the Qualifier interface method. func (q *intQualifier) Qualify(vars Activation, obj any) (any, error) { val, _, err := q.qualifyInternal(vars, obj, false, false) @@ -868,6 +952,7 @@ type uintQualifier struct { value uint64 celValue ref.Val adapter ref.TypeAdapter + optional bool } // ID is an implementation of the Qualifier interface method. @@ -875,6 +960,11 @@ func (q *uintQualifier) ID() int64 { return q.id } +// IsOptional implements the Qualifier interface method. +func (q *uintQualifier) IsOptional() bool { + return q.optional +} + // Qualify implements the Qualifier interface method. func (q *uintQualifier) Qualify(vars Activation, obj any) (any, error) { val, _, err := q.qualifyInternal(vars, obj, false, false) @@ -932,6 +1022,7 @@ type boolQualifier struct { value bool celValue ref.Val adapter ref.TypeAdapter + optional bool } // ID is an implementation of the Qualifier interface method. @@ -939,6 +1030,11 @@ func (q *boolQualifier) ID() int64 { return q.id } +// IsOptional implements the Qualifier interface method. +func (q *boolQualifier) IsOptional() bool { + return q.optional +} + // Qualify implements the Qualifier interface method. func (q *boolQualifier) Qualify(vars Activation, obj any) (any, error) { val, _, err := q.qualifyInternal(vars, obj, false, false) @@ -985,6 +1081,7 @@ type fieldQualifier struct { Name string FieldType *ref.FieldType adapter ref.TypeAdapter + optional bool } // ID is an implementation of the Qualifier interface method. @@ -992,6 +1089,11 @@ func (q *fieldQualifier) ID() int64 { return q.id } +// IsOptional implements the Qualifier interface method. +func (q *fieldQualifier) IsOptional() bool { + return q.optional +} + // Qualify implements the Qualifier interface method. func (q *fieldQualifier) Qualify(vars Activation, obj any) (any, error) { if rv, ok := obj.(ref.Val); ok { @@ -1042,6 +1144,7 @@ type doubleQualifier struct { value float64 celValue ref.Val adapter ref.TypeAdapter + optional bool } // ID is an implementation of the Qualifier interface method. @@ -1049,6 +1152,11 @@ func (q *doubleQualifier) ID() int64 { return q.id } +// IsOptional implements the Qualifier interface method. +func (q *doubleQualifier) IsOptional() bool { + return q.optional +} + // Qualify implements the Qualifier interface method. func (q *doubleQualifier) Qualify(vars Activation, obj any) (any, error) { val, _, err := q.qualifyInternal(vars, obj, false, false) @@ -1080,6 +1188,11 @@ func (q *unknownQualifier) ID() int64 { return q.id } +// IsOptional returns trivially false as an the unknown value is always returned. +func (q *unknownQualifier) IsOptional() bool { + return false +} + // Qualify returns the unknown value associated with this qualifier. func (q *unknownQualifier) Qualify(vars Activation, obj any) (any, error) { return q.value, nil @@ -1095,13 +1208,46 @@ func (q *unknownQualifier) Value() ref.Val { return q.value } +func applyQualifiers(vars Activation, obj any, qualifiers []Qualifier) (any, bool, error) { + optObj, isOpt := obj.(*types.Optional) + if isOpt { + if !optObj.HasValue() { + return optObj, false, nil + } + obj = optObj.GetValue().Value() + } + + var err error + for _, qual := range qualifiers { + var qualObj any + isOpt = isOpt || qual.IsOptional() + if isOpt { + var present bool + qualObj, present, err = qual.QualifyIfPresent(vars, obj, false) + if err != nil { + return nil, false, err + } + if !present { + return types.OptionalNone, false, nil + } + } else { + qualObj, err = qual.Qualify(vars, obj) + if err != nil { + return nil, false, err + } + } + obj = qualObj + } + return obj, isOpt, nil +} + // attrQualify performs a qualification using the result of an attribute evaluation. func attrQualify(fac AttributeFactory, vars Activation, obj any, qualAttr Attribute) (any, error) { val, err := qualAttr.Resolve(vars) if err != nil { return nil, err } - qual, err := fac.NewQualifier(nil, qualAttr.ID(), val) + qual, err := fac.NewQualifier(nil, qualAttr.ID(), val, qualAttr.IsOptional()) if err != nil { return nil, err } @@ -1116,7 +1262,7 @@ func attrQualifyIfPresent(fac AttributeFactory, vars Activation, obj any, qualAt if err != nil { return nil, false, err } - qual, err := fac.NewQualifier(nil, qualAttr.ID(), val) + qual, err := fac.NewQualifier(nil, qualAttr.ID(), val, qualAttr.IsOptional()) if err != nil { return nil, false, err } @@ -1235,3 +1381,17 @@ func (e *resolutionError) Error() string { func (e *resolutionError) Is(err error) bool { return err.Error() == e.Error() } + +func findMin(x, y int64) int64 { + if x < y { + return x + } + return y +} + +func findMax(x, y int64) int64 { + if x > y { + return x + } + return y +} diff --git a/interpreter/attributes_test.go b/interpreter/attributes_test.go index 52968e11..6445a156 100644 --- a/interpreter/attributes_test.go +++ b/interpreter/attributes_test.go @@ -15,6 +15,8 @@ package interpreter import ( + "errors" + "fmt" "reflect" "testing" @@ -51,9 +53,9 @@ func TestAttributesAbsoluteAttr(t *testing.T) { // acme.a.b[4][false] attr := attrs.AbsoluteAttribute(1, "acme.a") - qualB, _ := attrs.NewQualifier(nil, 2, "b") - qual4, _ := attrs.NewQualifier(nil, 3, uint64(4)) - qualFalse, _ := attrs.NewQualifier(nil, 4, false) + qualB := makeQualifier(t, attrs, nil, 2, "b") + qual4 := makeQualifier(t, attrs, nil, 3, uint64(4)) + qualFalse := makeQualifier(t, attrs, nil, 4, false) attr.AddQualifier(qualB) attr.AddQualifier(qual4) attr.AddQualifier(qualFalse) @@ -109,8 +111,8 @@ func TestAttributesRelativeAttr(t *testing.T) { // The expression being evaluated is: .a[-1][b] -> 42 op := NewConstValue(1, reg.NativeToValue(data)) attr := attrs.RelativeAttribute(1, op) - qualA, _ := attrs.NewQualifier(nil, 2, "a") - qualNeg1, _ := attrs.NewQualifier(nil, 3, int64(-1)) + qualA := makeQualifier(t, attrs, nil, 2, "a") + qualNeg1 := makeQualifier(t, attrs, nil, 3, int64(-1)) attr.AddQualifier(qualA) attr.AddQualifier(qualNeg1) attr.AddQualifier(attrs.AbsoluteAttribute(4, "b")) @@ -127,7 +129,7 @@ func TestAttributesRelativeAttr(t *testing.T) { } } -func TestAttributesRelativeAttr_OneOf(t *testing.T) { +func TestAttributesRelativeAttrOneOf(t *testing.T) { reg := newTestRegistry(t) cont, err := containers.NewContainer(containers.Name("acme.ns")) if err != nil { @@ -158,8 +160,8 @@ func TestAttributesRelativeAttr_OneOf(t *testing.T) { // The correct behavior should yield the value of the last alternative. op := NewConstValue(1, reg.NativeToValue(data)) attr := attrs.RelativeAttribute(1, op) - qualA, _ := attrs.NewQualifier(nil, 2, "a") - qualNeg1, _ := attrs.NewQualifier(nil, 3, int64(-1)) + qualA := makeQualifier(t, attrs, nil, 2, "a") + qualNeg1 := makeQualifier(t, attrs, nil, 3, int64(-1)) attr.AddQualifier(qualA) attr.AddQualifier(qualNeg1) attr.AddQualifier(attrs.MaybeAttribute(4, "b")) @@ -176,7 +178,7 @@ func TestAttributesRelativeAttr_OneOf(t *testing.T) { } } -func TestAttributesRelativeAttr_Conditional(t *testing.T) { +func TestAttributesRelativeAttrConditional(t *testing.T) { reg := newTestRegistry(t) attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) data := map[string]any{ @@ -203,13 +205,13 @@ func TestAttributesRelativeAttr_Conditional(t *testing.T) { condAttr := attrs.ConditionalAttribute(4, cond, attrs.AbsoluteAttribute(5, "b"), attrs.AbsoluteAttribute(6, "c")) - qual0, _ := attrs.NewQualifier(nil, 7, 0) + qual0 := makeQualifier(t, attrs, nil, 7, 0) condAttr.AddQualifier(qual0) obj := NewConstValue(1, reg.NativeToValue(data)) attr := attrs.RelativeAttribute(1, obj) - qualA, _ := attrs.NewQualifier(nil, 2, "a") - qualNeg1, _ := attrs.NewQualifier(nil, 3, int64(-1)) + qualA := makeQualifier(t, attrs, nil, 2, "a") + qualNeg1 := makeQualifier(t, attrs, nil, 3, int64(-1)) attr.AddQualifier(qualA) attr.AddQualifier(qualNeg1) attr.AddQualifier(condAttr) @@ -226,7 +228,7 @@ func TestAttributesRelativeAttr_Conditional(t *testing.T) { } } -func TestAttributesRelativeAttr_Relative(t *testing.T) { +func TestAttributesRelativeAttrRelativeQualifier(t *testing.T) { cont, err := containers.NewContainer(containers.Name("acme.ns")) if err != nil { t.Fatal(err) @@ -278,11 +280,11 @@ func TestAttributesRelativeAttr_Relative(t *testing.T) { 3: "third", })) relAttr := attrs.RelativeAttribute(4, mp) - qualB, _ := attrs.NewQualifier(nil, 5, attrs.AbsoluteAttribute(5, "b")) + qualB := makeQualifier(t, attrs, nil, 5, attrs.AbsoluteAttribute(5, "b")) relAttr.AddQualifier(qualB) attr := attrs.RelativeAttribute(1, obj) - qualA, _ := attrs.NewQualifier(nil, 2, "a") - qualNeg1, _ := attrs.NewQualifier(nil, 3, int64(-1)) + qualA := makeQualifier(t, attrs, nil, 2, "a") + qualNeg1 := makeQualifier(t, attrs, nil, 3, int64(-1)) attr.AddQualifier(qualA) attr.AddQualifier(qualNeg1) attr.AddQualifier(relAttr) @@ -318,7 +320,7 @@ func TestAttributesOneofAttr(t *testing.T) { // a.b -> should resolve to acme.ns.a.b per namespace resolution rules. attr := attrs.MaybeAttribute(1, "a") - qualB, _ := attrs.NewQualifier(nil, 2, "b") + qualB := makeQualifier(t, attrs, nil, 2, "b") attr.AddQualifier(qualB) out, err := attr.Resolve(vars) if err != nil { @@ -333,7 +335,7 @@ func TestAttributesOneofAttr(t *testing.T) { } } -func TestAttributesConditionalAttr_TrueBranch(t *testing.T) { +func TestAttributesConditionalAttrTrueBranch(t *testing.T) { reg := newTestRegistry(t) attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) data := map[string]any{ @@ -351,11 +353,11 @@ func TestAttributesConditionalAttr_TrueBranch(t *testing.T) { // (true ? a : b.c)[-1][1] tv := attrs.AbsoluteAttribute(2, "a") fv := attrs.MaybeAttribute(3, "b") - qualC, _ := attrs.NewQualifier(nil, 4, "c") + qualC := makeQualifier(t, attrs, nil, 4, "c") fv.AddQualifier(qualC) cond := attrs.ConditionalAttribute(1, NewConstValue(0, types.True), tv, fv) - qualNeg1, _ := attrs.NewQualifier(nil, 5, int64(-1)) - qual1, _ := attrs.NewQualifier(nil, 6, int64(1)) + qualNeg1 := makeQualifier(t, attrs, nil, 5, int64(-1)) + qual1 := makeQualifier(t, attrs, nil, 6, int64(1)) cond.AddQualifier(qualNeg1) cond.AddQualifier(qual1) out, err := cond.Resolve(vars) @@ -371,7 +373,7 @@ func TestAttributesConditionalAttr_TrueBranch(t *testing.T) { } } -func TestAttributesConditionalAttr_FalseBranch(t *testing.T) { +func TestAttributesConditionalAttrFalseBranch(t *testing.T) { reg := newTestRegistry(t) attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) data := map[string]any{ @@ -389,11 +391,11 @@ func TestAttributesConditionalAttr_FalseBranch(t *testing.T) { // (false ? a : b.c)[-1][1] tv := attrs.AbsoluteAttribute(2, "a") fv := attrs.MaybeAttribute(3, "b") - qualC, _ := attrs.NewQualifier(nil, 4, "c") + qualC := makeQualifier(t, attrs, nil, 4, "c") fv.AddQualifier(qualC) cond := attrs.ConditionalAttribute(1, NewConstValue(0, types.False), tv, fv) - qualNeg1, _ := attrs.NewQualifier(nil, 5, int64(-1)) - qual1, _ := attrs.NewQualifier(nil, 6, int64(1)) + qualNeg1 := makeQualifier(t, attrs, nil, 5, int64(-1)) + qual1 := makeQualifier(t, attrs, nil, 6, int64(1)) cond.AddQualifier(qualNeg1) cond.AddQualifier(qual1) out, err := cond.Resolve(vars) @@ -409,7 +411,351 @@ func TestAttributesConditionalAttr_FalseBranch(t *testing.T) { } } -func TestAttributesConditionalAttr_ErrorUnknown(t *testing.T) { +func TestAttributesOptional(t *testing.T) { + reg := newTestRegistry(t, &proto3pb.TestAllTypes{}) + cont, err := containers.NewContainer(containers.Name("ns")) + if err != nil { + t.Fatalf("") + } + attrs := NewAttributeFactory(cont, reg, reg) + + tests := []struct { + varName string + quals []any + optQuals []any + vars map[string]any + out any + err error + }{ + { + // a.?b[0][false] + varName: "a", + optQuals: []any{"b", int32(0), false}, + vars: map[string]any{ + "a": map[string]any{ + "b": map[int]any{ + 0: map[bool]string{ + false: "success", + }, + }, + }, + }, + out: types.OptionalOf(reg.NativeToValue("success")), + }, + { + // a.?b[0][false] + varName: "a", + optQuals: []any{"b", uint32(0), false}, + vars: map[string]any{ + "a": map[string]any{ + "b": map[int]any{ + 0: map[bool]string{ + false: "success", + }, + }, + }, + }, + out: types.OptionalOf(reg.NativeToValue("success")), + }, + { + // a.?b[0][false] + varName: "a", + optQuals: []any{"b", float32(0), false}, + vars: map[string]any{ + "a": map[string]any{ + "b": map[int]any{ + 0: map[bool]string{ + false: "success", + }, + }, + }, + }, + out: types.OptionalOf(reg.NativeToValue("success")), + }, + { + // a.?b[1] with no value + varName: "a", + optQuals: []any{"b", uint(1)}, + vars: map[string]any{ + "a": map[string]any{ + "b": map[uint]any{}, + }, + }, + out: types.OptionalNone, + }, + { + // a.b[1] with no value where b is a map[uint] + varName: "a", + quals: []any{"b", uint(1)}, + vars: map[string]any{ + "a": map[string]any{ + "b": map[uint]any{}, + }, + }, + err: errors.New("no such key: 1"), + }, + { + // a.b[?1] with no value where 'b' is a []int + varName: "a", + quals: []any{"b"}, + optQuals: []any{1}, + vars: map[string]any{ + "a": map[string]any{ + "b": []int{}, + }, + }, + out: types.OptionalNone, + }, + { + // a.b[1] with no value where 'b' is a map[int]any + varName: "a", + quals: []any{"b", 1}, + vars: map[string]any{ + "a": map[string]any{ + "b": map[int]any{}, + }, + }, + err: errors.New("no such key: 1"), + }, + { + // a.b[?1] with no value where 'b' is a []int + varName: "a", + quals: []any{"b", 1, false}, + optQuals: []any{}, + vars: map[string]any{ + "a": map[string]any{ + "b": []int{}, + }, + }, + err: errors.New("index out of bounds: 1"), + }, + { + // a.?b[0][true] with no value + varName: "a", + optQuals: []any{"b", 0, false}, + vars: map[string]any{ + "a": map[string]any{ + "b": map[int]any{ + 0: map[bool]any{}, + }, + }, + }, + out: types.OptionalNone, + }, + { + // a.b[0][?true] with no value + varName: "a", + quals: []any{"b", 0}, + optQuals: []any{true}, + vars: map[string]any{ + "a": map[string]any{ + "b": map[int]any{ + 0: map[bool]any{}, + }, + }, + }, + out: types.OptionalNone, + }, + { + // a.b[0][true] with no value + varName: "a", + quals: []any{"b", 0, true}, + vars: map[string]any{ + "a": map[string]any{ + "b": map[int]any{ + 0: map[bool]any{}, + }, + }, + }, + err: errors.New("no such key: true"), + }, + { + // a.b[0][false] where 'a' is optional + varName: "a", + quals: []any{"b", int32(0), false}, + vars: map[string]any{ + "a": types.OptionalOf(reg.NativeToValue(map[string]any{ + "b": map[int]any{ + 0: map[bool]string{ + false: "success", + }, + }, + })), + }, + out: types.OptionalOf(reg.NativeToValue("success")), + }, + { + // a.b[0][false] where 'a' is optional none. + varName: "a", + quals: []any{"b", int32(0), false}, + vars: map[string]any{ + "a": types.OptionalNone, + }, + out: types.OptionalNone, + }, + { + // a.?c[1][true] + varName: "a", + optQuals: []any{"c", int32(1), true}, + vars: map[string]any{ + "a": map[string]any{}, + }, + out: types.OptionalNone, + }, + { + // a[?b] where 'b' is dynamically computed. + varName: "a", + optQuals: []any{attrs.AbsoluteAttribute(0, "b")}, + vars: map[string]any{ + "a": map[string]any{ + "hello": "world", + }, + "b": "hello", + }, + out: types.OptionalOf(reg.NativeToValue("world")), + }, + { + // a[?(false ? : b : c.d.e)] + varName: "a", + optQuals: []any{ + attrs.ConditionalAttribute(0, + NewConstValue(100, types.False), + attrs.AbsoluteAttribute(101, "b"), + attrs.MaybeAttribute(102, "c.d.e")), + }, + vars: map[string]any{ + "a": map[string]any{ + "hello": "world", + "goodbye": "universe", + }, + "b": "hello", + "c.d.e": "goodbye", + }, + out: types.OptionalOf(reg.NativeToValue("universe")), + }, + { + // a[?c.d.e] + varName: "a", + optQuals: []any{ + attrs.MaybeAttribute(102, "c.d.e"), + }, + vars: map[string]any{ + "a": map[string]any{ + "hello": "world", + "goodbye": "universe", + }, + "b": "hello", + "c.d.e": "goodbye", + }, + out: types.OptionalOf(reg.NativeToValue("universe")), + }, + { + // a[c.d.e] where the c.d.e errors + varName: "a", + quals: []any{ + addQualifier(t, attrs.MaybeAttribute(102, "c.d"), makeQualifier(t, attrs, nil, 103, "e")), + }, + vars: map[string]any{ + "a": map[string]any{ + "goodbye": "universe", + }, + "c.d": map[string]any{}, + }, + err: errors.New("no such key: e"), + }, + { + // a[?c.d.e] where the c.d.e errors + varName: "a", + optQuals: []any{ + addQualifier(t, attrs.MaybeAttribute(102, "c.d"), makeQualifier(t, attrs, nil, 103, "e")), + }, + vars: map[string]any{ + "a": map[string]any{ + "goodbye": "universe", + }, + "c.d": map[string]any{}, + }, + err: errors.New("no such key: e"), + }, + { + // a.?single_int32 with a value. + varName: "a", + optQuals: []any{"single_int32"}, + vars: map[string]any{ + "a": &proto3pb.TestAllTypes{SingleInt32: 1}, + }, + out: types.OptionalOf(reg.NativeToValue(1)), + }, + { + // a.?single_int32 where the field is not set. + varName: "a", + optQuals: []any{"single_int32"}, + vars: map[string]any{ + "a": &proto3pb.TestAllTypes{}, + }, + out: types.OptionalNone, + }, + { + // a.?single_int32 where the field is set (uses more optimal selection logic) + varName: "a", + optQuals: []any{ + makeOptQualifier(t, + attrs, + &exprpb.Type{TypeKind: &exprpb.Type_MessageType{MessageType: "google.expr.proto3.test.TestAllTypes"}}, + 103, + "single_int32", + ), + }, + vars: map[string]any{ + "a": &proto3pb.TestAllTypes{SingleInt32: 1}, + }, + out: types.OptionalOf(reg.NativeToValue(1)), + }, + { + // a.c[1][true] + varName: "a", + quals: []any{"c", int32(1), true}, + vars: map[string]any{ + "a": map[string]any{}, + }, + err: errors.New("no such key: c"), + }, + } + for i, tst := range tests { + tc := tst + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + i := int64(1) + attr := attrs.AbsoluteAttribute(i, tc.varName) + for _, q := range tc.quals { + i++ + attr.AddQualifier(makeQualifier(t, attrs, nil, i, q)) + } + for _, oq := range tc.optQuals { + i++ + attr.AddQualifier(makeOptQualifier(t, attrs, nil, i, oq)) + } + vars, err := NewActivation(tc.vars) + if err != nil { + t.Fatalf("NewActivation() failed: %v", err) + } + out, err := attr.Resolve(vars) + if err != nil { + if tc.err != nil { + if tc.err.Error() == err.Error() { + return + } + t.Fatalf("attr.Resolve() errored with %v, wanted error %v", err, tc.err) + } + t.Fatalf("attr.Resolve() failed: %v", err) + } + if !reflect.DeepEqual(out, tc.out) { + t.Errorf("attr.Resolve() got %v, wanted %v", out, tc.out) + } + }) + } +} + +func TestAttributesConditionalAttrErrorUnknown(t *testing.T) { reg := newTestRegistry(t) attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) @@ -487,11 +833,12 @@ func TestResolverCustomQualifier(t *testing.T) { "msg": msg, }) attr := attrs.AbsoluteAttribute(1, "msg") - qualBB, _ := attrs.NewQualifier(&exprpb.Type{ + fieldType := &exprpb.Type{ TypeKind: &exprpb.Type_MessageType{ MessageType: "google.expr.proto3.test.TestAllTypes.NestedMessage", }, - }, 2, "bb") + } + qualBB := makeQualifier(t, attrs, fieldType, 2, "bb") attr.AddQualifier(qualBB) out, err := attr.Resolve(vars) if err != nil { @@ -516,7 +863,7 @@ func TestAttributesMissingMsg(t *testing.T) { // missing_msg.field attr := attrs.AbsoluteAttribute(1, "missing_msg") - field, _ := attrs.NewQualifier(nil, 2, "field") + field := makeQualifier(t, attrs, nil, 2, "field") attr.AddQualifier(field) out, err := attr.Resolve(vars) if err == nil { @@ -527,7 +874,7 @@ func TestAttributesMissingMsg(t *testing.T) { } } -func TestAttributeMissingMsg_UnknownField(t *testing.T) { +func TestAttributeMissingMsgUnknownField(t *testing.T) { reg := newTestRegistry(t) attrs := NewPartialAttributeFactory(containers.DefaultContainer, reg, reg) anyPB, _ := anypb.New(&proto3pb.TestAllTypes{}) @@ -537,7 +884,7 @@ func TestAttributeMissingMsg_UnknownField(t *testing.T) { // missing_msg.field attr := attrs.AbsoluteAttribute(1, "missing_msg") - field, _ := attrs.NewQualifier(nil, 2, "field") + field := makeQualifier(t, attrs, nil, 2, "field") attr.AddQualifier(field) out, err := attr.Resolve(vars) if err != nil { @@ -728,11 +1075,12 @@ func BenchmarkResolverCustomQualifier(b *testing.B) { "msg": msg, }) attr := attrs.AbsoluteAttribute(1, "msg") - qualBB, _ := attrs.NewQualifier(&exprpb.Type{ + fieldType := &exprpb.Type{ TypeKind: &exprpb.Type_MessageType{ MessageType: "google.expr.proto3.test.TestAllTypes.NestedMessage", }, - }, 2, "bb") + } + qualBB := makeQualifier(b, attrs, fieldType, 2, "bb") attr.AddQualifier(qualBB) for i := 0; i < b.N; i++ { attr.Resolve(vars) @@ -743,12 +1091,11 @@ type custAttrFactory struct { AttributeFactory } -func (r *custAttrFactory) NewQualifier(objType *exprpb.Type, - qualID int64, val any) (Qualifier, error) { +func (r *custAttrFactory) NewQualifier(objType *exprpb.Type, qualID int64, val any, opt bool) (Qualifier, error) { if objType.GetMessageType() == "google.expr.proto3.test.TestAllTypes.NestedMessage" { return &nestedMsgQualifier{id: qualID, field: val.(string)}, nil } - return r.AttributeFactory.NewQualifier(objType, qualID, val) + return r.AttributeFactory.NewQualifier(objType, qualID, val, opt) } type nestedMsgQualifier struct { @@ -773,16 +1120,47 @@ func (q *nestedMsgQualifier) QualifyIfPresent(vars Activation, obj any, presence return pb.GetBb(), true, nil } +func (q *nestedMsgQualifier) IsOptional() bool { + return false +} + // Cost implements the Coster interface method. It returns zero for testing purposes. func (q *nestedMsgQualifier) Cost() (min, max int64) { return 0, 0 } -func makeQualifier(b *testing.B, attrs AttributeFactory, typ *exprpb.Type, qualID int64, val any) Qualifier { - b.Helper() - qual, err := attrs.NewQualifier(typ, qualID, val) +func addQualifier(t testing.TB, attr Attribute, qual Qualifier) Attribute { + t.Helper() + _, err := attr.AddQualifier(qual) + if err != nil { + t.Fatalf("attr.AddQualifier(%v) failed: %v", qual, err) + } + return attr +} + +func makeQualifier(t testing.TB, attrs AttributeFactory, fieldType *exprpb.Type, qualID int64, val any) Qualifier { + t.Helper() + qual, err := attrs.NewQualifier(fieldType, qualID, val, false) + if err != nil { + t.Fatalf("attrs.NewQualifier() failed: %v", err) + } + return qual +} + +func makeOptQualifier(t testing.TB, attrs AttributeFactory, fieldType *exprpb.Type, qualID int64, val any) Qualifier { + t.Helper() + qual, err := attrs.NewQualifier(fieldType, qualID, val, true) if err != nil { - b.Fatalf("attrs.NewQualifier() failed: %v", err) + t.Fatalf("attrs.NewQualifier() failed: %v", err) } return qual } + +func findField(t testing.TB, reg ref.TypeRegistry, typeName, field string) *ref.FieldType { + t.Helper() + ft, found := reg.FindFieldType(typeName, field) + if !found { + t.Fatalf("reg.FindFieldType(%v, %v) failed", typeName, field) + } + return ft +} diff --git a/interpreter/interpretable.go b/interpreter/interpretable.go index b85153e2..58659469 100644 --- a/interpreter/interpretable.go +++ b/interpreter/interpretable.go @@ -71,6 +71,8 @@ type InterpretableAttribute interface { // to whether the qualifier is present. QualifyIfPresent(vars Activation, obj any, presenceOnly bool) (any, bool, error) + IsOptional() bool + // Resolve returns the value of the Attribute given the current Activation. Resolve(Activation) (any, error) } @@ -1147,19 +1149,19 @@ func (cond *evalExhaustiveConditional) Eval(ctx Activation) ref.Val { cVal := cond.attr.expr.Eval(ctx) tVal, tErr := cond.attr.truthy.Resolve(ctx) fVal, fErr := cond.attr.falsy.Resolve(ctx) - if tErr != nil { - return types.NewErr(tErr.Error()) - } - if fErr != nil { - return types.NewErr(fErr.Error()) - } cBool, ok := cVal.(types.Bool) if !ok { return types.ValOrErr(cVal, "no such overload") } if cBool { + if tErr != nil { + return types.NewErr(tErr.Error()) + } return cond.adapter.NativeToValue(tVal) } + if fErr != nil { + return types.NewErr(fErr.Error()) + } return cond.adapter.NativeToValue(fVal) } @@ -1170,8 +1172,9 @@ func (cond *evalExhaustiveConditional) Cost() (min, max int64) { // evalAttr evaluates an Attribute value. type evalAttr struct { - adapter ref.TypeAdapter - attr Attribute + adapter ref.TypeAdapter + attr Attribute + optional bool } // ID of the attribute instruction. @@ -1220,6 +1223,10 @@ func (a *evalAttr) QualifyIfPresent(ctx Activation, obj any, presenceOnly bool) return a.attr.QualifyIfPresent(ctx, obj, presenceOnly) } +func (a *evalAttr) IsOptional() bool { + return a.optional +} + // Resolve proxies to the Attribute's Resolve method. func (a *evalAttr) Resolve(ctx Activation) (any, error) { return a.attr.Resolve(ctx) diff --git a/interpreter/planner.go b/interpreter/planner.go index d52ea7cb..dcc48685 100644 --- a/interpreter/planner.go +++ b/interpreter/planner.go @@ -206,20 +206,20 @@ func (p *planner) planSelect(expr *exprpb.Expr) (Interpretable, error) { // Establish the attribute reference. attr, isAttr := op.(InterpretableAttribute) if !isAttr { - attr, err = p.relativeAttr(op.ID(), op) + attr, err = p.relativeAttr(op.ID(), op, false) if err != nil { return nil, err } } // Build a qualifier for the attribute. - qual, err := p.attrFactory.NewQualifier(opType, expr.GetId(), sel.GetField()) + qual, err := p.attrFactory.NewQualifier(opType, expr.GetId(), sel.GetField(), false) if err != nil { return nil, err } // Return the test only eval expression. - if sel.TestOnly { + if sel.GetTestOnly() { return &evalTestOnly{ id: expr.GetId(), field: types.String(sel.GetField()), @@ -230,10 +230,7 @@ func (p *planner) planSelect(expr *exprpb.Expr) (Interpretable, error) { // Otherwise, append the qualifier on the attribute. _, err = attr.AddQualifier(qual) - if err != nil { - return nil, err - } - return attr, nil + return attr, err } // planCall creates a callable Interpretable while specializing for common functions and invocation @@ -278,7 +275,9 @@ func (p *planner) planCall(expr *exprpb.Expr) (Interpretable, error) { case operators.NotEquals: return p.planCallNotEqual(expr, args) case operators.Index: - return p.planCallIndex(expr, args) + return p.planCallIndex(expr, args, false) + case operators.OptSelect, operators.OptIndex: + return p.planCallIndex(expr, args, true) } // Otherwise, generate Interpretable calls specialized by argument count. @@ -479,38 +478,38 @@ func (p *planner) planCallConditional(expr *exprpb.Expr, args []Interpretable) ( // planCallIndex either extends an attribute with the argument to the index operation, or creates // a relative attribute based on the return of a function call or operation. -func (p *planner) planCallIndex(expr *exprpb.Expr, args []Interpretable) (Interpretable, error) { +func (p *planner) planCallIndex(expr *exprpb.Expr, args []Interpretable, optional bool) (Interpretable, error) { op := args[0] ind := args[1] - opAttr, err := p.relativeAttr(op.ID(), op) - if err != nil { - return nil, err - } opType := p.typeMap[expr.GetCallExpr().GetTarget().GetId()] - indConst, isIndConst := ind.(InterpretableConst) - if isIndConst { - qual, err := p.attrFactory.NewQualifier(opType, expr.GetId(), indConst.Value()) + + // Establish the attribute reference. + var err error + attr, isAttr := op.(InterpretableAttribute) + if !isAttr { + attr, err = p.relativeAttr(op.ID(), op, false) if err != nil { return nil, err } - _, err = opAttr.AddQualifier(qual) - return opAttr, err } - indAttr, isIndAttr := ind.(InterpretableAttribute) - if isIndAttr { - qual, err := p.attrFactory.NewQualifier(opType, expr.GetId(), indAttr) - if err != nil { - return nil, err - } - _, err = opAttr.AddQualifier(qual) - return opAttr, err + + // Construct the qualifier type. + var qual Qualifier + switch ind := ind.(type) { + case InterpretableConst: + qual, err = p.attrFactory.NewQualifier(opType, expr.GetId(), ind.Value(), optional) + case InterpretableAttribute: + qual, err = p.attrFactory.NewQualifier(opType, expr.GetId(), ind, optional) + default: + qual, err = p.relativeAttr(expr.GetId(), ind, optional) } - indQual, err := p.relativeAttr(expr.GetId(), ind) if err != nil { return nil, err } - _, err = opAttr.AddQualifier(indQual) - return opAttr, err + + // Add the qualifier to the attribute + _, err = attr.AddQualifier(qual) + return attr, err } // planCreateList generates a list construction Interpretable. @@ -736,14 +735,18 @@ func (p *planner) resolveFunction(expr *exprpb.Expr) (*exprpb.Expr, string, stri return target, fnName, "" } -func (p *planner) relativeAttr(id int64, eval Interpretable) (InterpretableAttribute, error) { +// relativeAttr indicates that the attribute in this case acts as a qualifier and as such needs to +// be observed to ensure that it's evaluation value is properly recorded for state tracking. +func (p *planner) relativeAttr(id int64, eval Interpretable, opt bool) (InterpretableAttribute, error) { eAttr, ok := eval.(InterpretableAttribute) if !ok { eAttr = &evalAttr{ - adapter: p.adapter, - attr: p.attrFactory.RelativeAttribute(id, eval), + adapter: p.adapter, + attr: p.attrFactory.RelativeAttribute(id, eval), + optional: opt, } } + // This looks like it should either decorate the new evalAttr node, or early return the InterpretableAttribute decAttr, err := p.decorate(eAttr, nil) if err != nil { return nil, err