// Binary operator evaluation shared by the interpreter and the compiled // path: string predicates (STARTS WITH / ENDS WITH / CONTAINS) and the // arithmetic operators (+, -, *, /) over numbers, strings, lists, and // temporals/durations, with checked integer arithmetic (overflow and // division by zero yield Null -- eval has no per-row error channel). Split // from funcs.go, which holds the scalar built-in functions.
package eval import ( ) // StrPred is a string predicate (STARTS WITH / ENDS WITH / CONTAINS): // Bool when both operands are strings, Null otherwise. Shared with the // compiled path. func ( ast.BinOp, , value.Value) value.Value { , := .AsStr() , := .AsStr() if ! || ! { return value.Null() } switch { case ast.OpStartsWith: return value.Bool(strings.HasPrefix(, )) case ast.OpEndsWith: return value.Bool(strings.HasSuffix(, )) default: // ast.OpContains return value.Bool(strings.Contains(, )) } } // Arith is +, -, *, /: string + concatenates; temporal +/- duration is a // calendar add; Int op Int is checked (overflow and division by zero // yield Null -- eval has no per-row error channel, so Null is the honest // result); mixed numerics coerce through float64, where division by zero // is also Null. Shared with the compiled path. func ( ast.BinOp, , value.Value) value.Value { if == ast.OpAdd && .Kind() == value.KindStr && .Kind() == value.KindStr { , := .AsStr() , := .AsStr() return value.Str( + ) } // List concatenation / append (openCypher `+`, mirroring the Rust // engine): list + list chains the elements, list + element appends, // element + list prepends. A null operand stays null via the // fallthrough. if == ast.OpAdd && !.IsNull() && !.IsNull() { , := .AsList() , := .AsList() switch { case && : := make([]value.Value, 0, len()+len()) = append(, ...) return value.List(append(, ...)) case : := make([]value.Value, 0, len()+1) = append(, ...) return value.List(append(, )) case : := make([]value.Value, 0, len()+1) = append(, ) return value.List(append(, ...)) } } // Temporal +/- Duration -> Temporal; + is commutative. An Int +/- // Duration reads the Int as epoch millis -- the stored form of a // temporal column, matching the comparison coercion -- and shifts it, // staying an Int (the Rust engine's tasks/151, BI Q17's // creationDate + duration(...)). if ( == ast.OpAdd || == ast.OpSub) && .Kind() == value.KindDuration && (.Kind() == value.KindTemporal || .Kind() == value.KindInt) { return applyDur(, , ) } if == ast.OpAdd && .Kind() == value.KindDuration && (.Kind() == value.KindTemporal || .Kind() == value.KindInt) { return applyDur(, , ) } // Temporal - Temporal -> Duration (ISO GQL datetime subtraction). Both // operands are epoch-millis, so the difference is an exact millisecond // duration (months/days stay 0 -- calendar decomposition of a // difference is ambiguous and ISO leaves it to DURATION_BETWEEN-style // functions). if == ast.OpSub && .Kind() == value.KindTemporal && .Kind() == value.KindTemporal { , , := .AsTemporal() , , := .AsTemporal() if := - ; ( < ) == ( > 0) || == 0 { return value.Duration(0, 0, ) } return value.Null() } // Duration +/- Duration -> Duration (componentwise, checked). if ( == ast.OpAdd || == ast.OpSub) && .Kind() == value.KindDuration && .Kind() == value.KindDuration { , , , := .AsDuration() , , , := .AsDuration() , := combineInt(, , ) , := combineInt(, , ) , := combineInt(, , ) if && && { return value.Duration(, , ) } return value.Null() } // Duration * Int (commutative) and Duration / Int -> Duration, // componentwise and exact. Fractional factors are rejected (Null): // scaling a calendar month by a fraction has no well-defined carry, so // only integral scaling is supported. if == ast.OpMul || == ast.OpDiv { , := , if == ast.OpMul && .Kind() == value.KindInt && .Kind() == value.KindDuration { , = , } if .Kind() == value.KindDuration && .Kind() == value.KindInt { , := .AsInt() if == ast.OpDiv && == 0 { return value.Null() } , , , := .AsDuration() , := scaleInt(, , ) , := scaleInt(, , ) , := scaleInt(, , ) if && && { return value.Duration(, , ) } return value.Null() } } if .Kind() == value.KindInt && .Kind() == value.KindInt { , := .AsInt() , := .AsInt() return checkedInt(, , ) } , := .AsFloat() , := .AsFloat() if ! || ! { return value.Null() } switch { case ast.OpAdd: return value.Float( + ) case ast.OpSub: return value.Float( - ) case ast.OpMul: return value.Float( * ) default: // ast.OpDiv if == 0.0 { return value.Null() } return value.Float( / ) } } // applyDur applies a duration to a temporal (sign from the operator), or // to an Int read as epoch millis (shifted, staying an Int). func applyDur(, value.Value, ast.BinOp) value.Value { , , , := .AsDuration() := int64(1) if == ast.OpSub { = -1 } if .Kind() == value.KindInt { , := .AsInt() , := ApplyDuration(, , , , ) if ! { return value.Null() } return value.Int() } , , := .AsTemporal() , := ApplyDuration(, , , , ) if ! { return value.Null() } return value.Temporal(, ) } // combineInt is checked int64 addition/subtraction, comma-ok. func combineInt( ast.BinOp, , int64) (int64, bool) { if == ast.OpSub { := - return , ( < ) == ( > 0) || == 0 } := + return , ( > ) == ( > 0) || == 0 } // scaleInt is checked int64 multiplication/division, comma-ok (the caller // rejects division by zero before calling). func scaleInt( ast.BinOp, , int64) (int64, bool) { if == ast.OpDiv { if == math.MinInt64 && == -1 { return 0, false } return / , true } if == 0 || == 0 { return 0, true } if ( == math.MinInt64 && == -1) || ( == math.MinInt64 && == -1) { return 0, false } := * return , / == } // checkedInt is checked 64-bit integer arithmetic: overflow (including // MinInt64 negation cases) and division by zero yield Null rather than a // wrapped wrong value. func checkedInt( ast.BinOp, , int64) value.Value { switch { case ast.OpAdd: := + if ( > ) == ( > 0) || == 0 { return value.Int() } case ast.OpSub: := - if ( < ) == ( > 0) || == 0 { return value.Int() } case ast.OpMul: if == 0 || == 0 { return value.Int(0) } if ( == math.MinInt64 && == -1) || ( == math.MinInt64 && == -1) { return value.Null() } := * if / == { return value.Int() } default: // ast.OpDiv if == 0 || ( == math.MinInt64 && == -1) { return value.Null() } return value.Int( / ) } return value.Null() }