// The arithmetic/string-predicate scalar-function kernels, shared verbatim // by the interpreter and the compiled path so both produce identical // results. No scalar function touches the graph except startNode/endNode, // which resolve in evalScalarFunc. The FuncOp enumeration and name // resolution live in funcop.go.
package eval import ( ) // evalScalarFunc evaluates a non-aggregate function call. Aggregates never // reach here -- they are extracted at plan time. An all-literal call is // memoized on the Ctx (see constCalls) so hot correlated evaluation -- // subquery WHEREs, list-predicate bodies -- pays its construction once per // execution. func evalScalarFunc( *Ctx, *ast.Func, []value.Value, map[string]int) value.Value { if , := .constCalls[]; && != nil { return * } else if ! { := !.Star for , := range .Args { if , := .(*ast.Lit); ! { = false break } } if .constCalls == nil { .constCalls = map[*ast.Func]*value.Value{} } if { := evalScalarFuncUncached(, , , ) .constCalls[] = & return } .constCalls[] = nil } return evalScalarFuncUncached(, , , ) } func evalScalarFuncUncached( *Ctx, *ast.Func, []value.Value, map[string]int) value.Value { // The argument row is a frame on the Ctx's argv stack rather than a // fresh slice per call: a nested call inside an argument pushes and // pops a deeper frame before this one continues filling, and no // ApplyFunc arm retains the argv slice (arms read elements by value or // build fresh output), so popping on return is safe. AST nodes are // shared across concurrent cached runs; the stack lives on the // per-execution Ctx, never the node. var []value.Value := len(.argvStack) if !.Star { .argvStack = slices.Grow(.argvStack, len(.Args))[:+len(.Args)] = .argvStack[ : +len(.Args)] for , := range .Args { [] = Eval(, , , ) } } := applyScalarFunc(, , ) .argvStack = .argvStack[:] return } func applyScalarFunc( *Ctx, *ast.Func, []value.Value) value.Value { // startNode(r)/endNode(r)/type(r) need the graph to resolve a // relationship from its CSR position, so they resolve here rather than in // the graph-less ApplyFunc. switch strings.ToLower(.Name) { case "startnode", "endnode": if len() > 0 { if , := [0].AsRel(); { if , , := .G.RelEndpoints(); { if strings.ToLower(.Name) == "startnode" { return value.Node() } return value.Node() } } } return value.Null() case "type": if len() > 0 { if , := [0].AsRel(); { if , := .G.RelTypeAt(); { return value.Str() } } } return value.Null() case "labels": // Composed from the label registry + membership: label counts are // small, so the per-call sweep beats adding a per-node enumeration // to the graph seam. if len() > 0 { if , := [0].AsNode(); { var []value.Value for , := range .G.LabelNames() { if .G.HasLabel(, ) { = append(, value.Str()) } } return value.List() } } return value.Null() } if , := ResolveFuncOp(.Name); { return ApplyFunc(, ) } return value.Null() } // ApplyFunc applies a resolved scalar function to its evaluated arguments. func ( FuncOp, []value.Value) value.Value { switch { case FuncDate: // date(x) -> Temporal(Date): an ISO string, an i64 epoch-millis, // another temporal, or a component map, truncated to midnight UTC. // Component accessors and ordering come with the temporal kind; the // retired YYYYMMDD-integer form read .year as an epoch and nulled // the int/temporal arguments BI Q16 itself uses. if := buildDatetime(arg(, 0), value.Date); .Kind() == value.KindTemporal { , , := .AsTemporal() return value.Temporal(floorDiv(, MSPerDay)*MSPerDay, value.Date) } return value.Null() case FuncDateTime: return buildDatetime(arg(, 0), value.DateTime) case FuncLocalDateTime: return buildDatetime(arg(, 0), value.LocalDateTime) case FuncDuration: if , := arg(, 0).AsMap(); { return buildDuration() } if , := arg(, 0).AsStr(); { if , , , := ParseISODuration(); { return value.Duration(, , ) } } return value.Null() case FuncID: // id(node)/id(rel) -- the internal identity as an integer. if , := arg(, 0).AsNode(); { return value.Int(int64()) } if , := arg(, 0).AsRel(); { return value.Int(int64()) } return value.Null() case FuncLength: // length(path) -- relationship count. if , , := arg(, 0).AsPath(); && len() > 0 { return value.Int(int64(len() - 1)) } return value.Null() case FuncNodes: if , , := arg(, 0).AsPath(); { := make([]value.Value, len()) for , := range { [] = value.Node() } return value.List() } return value.Null() case FuncRels: // rels(x): a path's relationship list; a var-length rel variable // (already a list) as-is; a single rel as a one-element list. := arg(, 0) if , , := .AsPath(); { := make([]value.Value, len()) for , := range { [] = value.Rel() } return value.List() } if , := .AsList(); { return } if , := .AsRel(); { return value.List([]value.Value{}) } return value.Null() case FuncSize: // size(list)/size(string) -- element or character count. if , := arg(, 0).AsList(); { return value.Int(int64(len())) } if , := arg(, 0).AsStr(); { return value.Int(int64(utf8.RuneCountInString())) } return value.Null() case FuncRange: return applyRange() case FuncLeft, FuncRight: , := arg(, 0).AsStr() , := arg(, 1).AsInt() if ! || ! || < 0 { return value.Null() } := []rune() := min(int(), len()) if == FuncLeft { return value.Str(string([:])) } return value.Str(string([len()-:])) case FuncSubstring: return applySubstring() case FuncAbs: if , := arg(, 0).AsInt(); { if == math.MinInt64 { return value.Null() } if < 0 { = - } return value.Int() } if arg(, 0).Kind() == value.KindFloat { , := arg(, 0).AsFloat() return value.Float(math.Abs()) } return value.Null() case FuncCeil, FuncFloor, FuncRound, FuncSqrt: , := numArg() if ! { return value.Null() } switch { case FuncCeil: return value.Float(math.Ceil()) case FuncFloor: return value.Float(math.Floor()) case FuncRound: // Half away from zero (Rust f64::round, the openCypher default). return value.Float(math.Round()) default: return value.Float(math.Sqrt()) } case FuncSign: , := numArg() if ! { return value.Null() } switch { case > 0: return value.Int(1) case < 0: return value.Int(-1) } return value.Int(0) case FuncToFloat: := arg(, 0) if .Kind() == value.KindInt || .Kind() == value.KindFloat { , := .AsFloat() return value.Float() } if , := .AsStr(); { if , := strconv.ParseFloat(strings.TrimSpace(), 64); == nil { return value.Float() } } return value.Null() case FuncToInteger: := arg(, 0) if , := .AsInt(); { return value.Int() } if .Kind() == value.KindFloat { , := .AsFloat() return value.Int(int64()) } if , := .AsStr(); { := strings.TrimSpace() if , := strconv.ParseInt(, 10, 64); == nil { return value.Int() } if , := strconv.ParseFloat(, 64); == nil { return value.Int(int64()) } } return value.Null() case FuncToString: return applyToString(arg(, 0)) case FuncToBoolean: := arg(, 0) if , := .AsBool(); { return value.Bool() } if , := .AsStr(); { switch strings.ToLower(strings.TrimSpace()) { case "true": return value.Bool(true) case "false": return value.Bool(false) } } return value.Null() case FuncCoalesce: for , := range { if !.IsNull() { return } } return value.Null() case FuncLower, FuncUpper: , := arg(, 0).AsStr() if ! { return value.Null() } if == FuncLower { return value.Str(strings.ToLower()) } return value.Str(strings.ToUpper()) case FuncCharLength: if , := arg(, 0).AsStr(); { return value.Int(int64(utf8.RuneCountInString())) } return value.Null() case FuncCardinality: if , := arg(, 0).AsList(); { return value.Int(int64(len())) } if , := arg(, 0).AsMap(); { return value.Int(int64(len())) } return value.Null() case FuncNormalize, FuncIsNormalized: // NORMALIZE(s [, form]) / s IS [NOT] [form] NORMALIZED (the // predicate lowers to is_normalized at parse time). Null // propagates; an unknown form name is a null, not an error, per // the engine's unknown-input convention. , := arg(, 0).AsStr() if ! { return value.Null() } := unorm.NFC if len() > 1 { , := arg(, 1).AsStr() if ! { return value.Null() } , := unorm.ParseForm() if ! { return value.Null() } = } if == FuncNormalize { return value.Str(unorm.Normalize(, )) } return value.Bool(unorm.IsNormalized(, )) case FuncTrim, FuncLTrim, FuncRTrim: , := arg(, 0).AsStr() if ! { return value.Null() } switch { case FuncTrim: return value.Str(strings.TrimSpace()) case FuncLTrim: return value.Str(strings.TrimLeft(, " \t\n\r")) default: return value.Str(strings.TrimRight(, " \t\n\r")) } case FuncMod: if , := arg(, 0).AsInt(); { if , := arg(, 1).AsInt(); && != 0 { return value.Int( % ) } if , := arg(, 1).AsFloat(); && != 0 { return value.Float(math.Mod(float64(), )) } return value.Null() } if , := arg(, 0).AsFloat(); { if , := arg(, 1).AsFloat(); && != 0 { return value.Float(math.Mod(, )) } } return value.Null() case FuncPower: , := arg(, 0).AsFloat() , := arg(, 1).AsFloat() if ! || ! { return value.Null() } return value.Float(math.Pow(, )) case FuncExp, FuncLn, FuncLog10, FuncSin, FuncCos, FuncTan, FuncAsin, FuncAcos, FuncAtan, FuncDegrees, FuncRadians: , := arg(, 0).AsFloat() if ! { return value.Null() } var float64 switch { case FuncExp: = math.Exp() case FuncLn: = math.Log() case FuncLog10: = math.Log10() case FuncSin: = math.Sin() case FuncCos: = math.Cos() case FuncTan: = math.Tan() case FuncAsin: = math.Asin() case FuncAcos: = math.Acos() case FuncAtan: = math.Atan() case FuncDegrees: = * 180 / math.Pi default: = * math.Pi / 180 } if math.IsNaN() || math.IsInf(, 0) { return value.Null() } return value.Float() case FuncNullIf: , := arg(, 0), arg(, 1) if value.Equal(, ) { return value.Null() } return case FuncHead: if , := arg(, 0).AsList(); && len() > 0 { return [0] } return value.Null() case FuncLast: if , := arg(, 0).AsList(); && len() > 0 { return [len()-1] } return value.Null() case FuncTail: if , := arg(, 0).AsList(); { if len() <= 1 { return value.List(nil) } := make([]value.Value, len()-1) copy(, [1:]) return value.List() } return value.Null() case FuncElementID: if , := arg(, 0).AsNode(); { return value.Str(strconv.FormatUint(uint64(), 10)) } if , := arg(, 0).AsRel(); { return value.Str("r" + strconv.FormatUint(uint64(), 10)) } return value.Null() } return value.Null() }