// Comparison semantics ported from the Rust cypher crate's value.rs: the // three-valued Compare/Equal used by =, <, >, IN and friends; the Kleene // combinators behind AND/OR; and the genuinely total OrderCmp for ORDER BY.
package value import ( ) // Compare is the three-valued comparison behind =, <>, <, <=, >, >=: ok is // false when either side is Null or the types are not comparable. Numbers // compare across Int/Float by coercing through float64 (including Int/Int, // mirroring the Rust engine exactly -- lossy above 2^53 only). func (, Value) (int, bool) { if .kind == KindNull || .kind == KindNull { return 0, false } switch { case .kind == KindBool && .kind == KindBool: return cmpUint(.num, .num), true case .kind == KindStr && .kind == KindStr: return cmpStr(.str, .str), true case .kind == KindNode && .kind == KindNode, .kind == KindRel && .kind == KindRel: return cmpUint(.num, .num), true case .kind == KindList && .kind == KindList: return compareLists(.ext.list, .ext.list) case .kind == KindMap && .kind == KindMap: // Maps are equatable (content, order-independent) but not ordered: // equal -> 0, otherwise incomparable, so =/<>/DISTINCT work and // </> on maps yield null. if mapsEqual(.ext.entries, .ext.entries) { return 0, true } return 0, false case .kind == KindPath && .kind == KindPath: // Paths are equatable (same node and rel sequences) but not ordered. if slicesEqual(.ext.nodes, .ext.nodes) && slicesEqual(.ext.rels, .ext.rels) { return 0, true } return 0, false // Temporals order by epoch millis and coerce against an i64 column (the // stored epoch-millis value) so creationDate < datetime(...) works. case .kind == KindTemporal && .kind == KindTemporal, .kind == KindTemporal && .kind == KindInt, .kind == KindInt && .kind == KindTemporal: return cmpInt(int64(.num), int64(.num)), true case .kind == KindDuration && .kind == KindDuration: // Durations are equatable by component but not ordered (months vs // days is ambiguous). if .ext.months == .ext.months && .ext.days == .ext.days && .num == .num { return 0, true } return 0, false } , := .asNum() , := .asNum() if ! || ! { return 0, false } // float64 partial comparison: NaN on either side is incomparable. switch { case < : return -1, true case > : return 1, true case == : return 0, true } return 0, false } // Equal reports three-valued equality collapsed to a boolean: true only // when Compare says equal. func (, Value) bool { , := Compare(, ) return && == 0 } // compareLists compares element-wise via Compare, then by length; an // incomparable element pair makes the lists incomparable. func compareLists(, []Value) (int, bool) { for := range min(len(), len()) { , := Compare([], []) if ! || != 0 { return , } } return cmpInt(int64(len()), int64(len())), true } // mapsEqual reports whether two maps have the same key set with equal // values -- independent of insertion order (GQL map equality). func mapsEqual(, []MapEntry) bool { if len() != len() { return false } for , := range { := false for , := range { if .Key == .Key { = Equal(.Val, .Val) break } } if ! { return false } } return true } // ThreeValued maps a value to a Kleene truth value for AND/OR: a Bool is // its value, Null is unknown (known=false). A non-boolean operand maps to // false, preserving the engine's leniency (only Bool(true) was ever true). func ( Value) (, bool) { switch .kind { case KindBool: return .num != 0, true case KindNull: return false, false default: return false, true } } // KleeneAnd is three-valued AND over ThreeValued operands: false dominates, // else Null if either side is unknown, else true. func (, , , bool) Value { switch { case && !, && !: return Bool(false) case && : return Bool(true) } return Null() } // KleeneOr is three-valued OR over ThreeValued operands: true dominates, // else Null if either side is unknown, else false. func (, , , bool) Value { switch { case && , && : return Bool(true) case && : return Bool(false) } return Null() } // OrderCmp is a genuine total order for ORDER BY: values of different type // categories order by a stable type rank (Null last), and same-category // values order naturally. Deliberately NOT Compare (a partial three-valued // relation): NaN is placed after finite numbers via a total float order, // and Temporal is never cross-compared with numbers (which produced an // intransitive cycle in the partial relation); each category is a // self-contained tier. func (, Value) int { , := orderRank(), orderRank() if != { return cmpInt(int64(), int64()) } switch .kind { case KindNull: return 0 case KindBool: return cmpUint(.num, .num) case KindInt, KindFloat: // Int and Float share a tier; compare numerically with a total // order over float64 (NaN sorts after finite values). int64 -> // float64 is lossy for very large magnitudes, but only as an ORDER // BY tiebreak (equality uses Compare). return totalCmpF64(orderNum(), orderNum()) case KindStr: return cmpStr(.str, .str) case KindList: , := .ext.list, .ext.list for := range min(len(), len()) { if := ([], []); != 0 { return } } return cmpInt(int64(len()), int64(len())) case KindNode, KindRel: return cmpUint(.num, .num) case KindPath: if := cmpSlices(.ext.nodes, .ext.nodes); != 0 { return } return cmpSlices(.ext.rels, .ext.rels) case KindMap: return mapOrder(.ext.entries, .ext.entries) case KindTemporal: if := cmpInt(int64(.num), int64(.num)); != 0 { return } return cmpInt(int64(.aux), int64(.aux)) case KindDuration: if := cmpInt(.ext.months, .ext.months); != 0 { return } if := cmpInt(.ext.days, .ext.days); != 0 { return } return cmpInt(int64(.num), int64(.num)) } return 0 } // orderRank is OrderCmp's type-tier table (Null last). func orderRank( Value) uint8 { switch .kind { case KindBool: return 0 case KindInt, KindFloat: return 1 case KindStr: return 2 case KindList: return 3 case KindNode: return 4 case KindRel: return 5 case KindPath: return 6 case KindMap: return 7 case KindTemporal: return 8 case KindDuration: return 9 } return math.MaxUint8 } // orderNum reads the numeric tier's payload, NaN if somehow non-numeric. func orderNum( Value) float64 { , := .asNum() if ! { return math.NaN() } return } // totalCmpF64 is IEEE-754 totalOrder (the Rust f64::total_cmp): -NaN < // -Inf < ... < -0.0 < +0.0 < ... < +Inf < +NaN. func totalCmpF64(, float64) int { := int64(math.Float64bits()) := int64(math.Float64bits()) ^= ( >> 63) & math.MaxInt64 ^= ( >> 63) & math.MaxInt64 return cmpInt(, ) } // TotalOrderF64 exposes OrderCmp's numeric-tier total order (IEEE-754 // totalOrder) so typed sort-key paths compare through the identical // kernel instead of a re-implementation. func (, float64) int { return totalCmpF64(, ) } // OrderNumF64 exposes OrderCmp's numeric-tier encoding of an Int/Float // value (NaN for anything else), pairing with TotalOrderF64. func ( Value) float64 { return orderNum() } // mapOrder is a total order over maps for OrderCmp: compare entries in // key-sorted order by (key, value), then by size, so two maps order // deterministically regardless of insertion order (consistent with the // order-independent map equality in Compare). func mapOrder(, []MapEntry) int { , := keySorted(), keySorted() for := range min(len(), len()) { , := [[]], [[]] if := cmpStr(.Key, .Key); != 0 { return } if := OrderCmp(.Val, .Val); != 0 { return } } return cmpInt(int64(len()), int64(len())) } // keySorted returns the entry indices of m in ascending key order. func keySorted( []MapEntry) []int { := make([]int, len()) for := range { [] = } sort.Slice(, func(, int) bool { return [[]].Key < [[]].Key }) return } func cmpInt(, int64) int { switch { case < : return -1 case > : return 1 } return 0 } func cmpUint(, uint64) int { switch { case < : return -1 case > : return 1 } return 0 } func cmpStr(, string) int { switch { case < : return -1 case > : return 1 } return 0 } // cmpSlices compares uint32 slices lexicographically then by length. func cmpSlices[ ~uint32](, []) int { for := range min(len(), len()) { if := cmpUint(uint64([]), uint64([])); != 0 { return } } return cmpInt(int64(len()), int64(len())) } // slicesEqual reports element-wise equality of uint32 slices. func slicesEqual[ ~uint32](, []) bool { if len() != len() { return false } for := range { if [] != [] { return false } } return true } // SameBacking reports payload identity in O(1): ext-carrying kinds (list, // map, path, duration) compare their shared ext pointer, not content. A // true result guarantees Identical; false says nothing. Used to skip // recomputing derived state (e.g. an IN membership index) when a carried // value is literally the same value as last time. func (, Value) bool { return .kind == .kind && .aux == .aux && .num == .num && .str == .str && .ext == .ext } // Identical is bit-exact value identity, unlike Equal: Null equals Null, // floats compare by bit pattern, and no numeric coercion applies. Used to // decide whether a slot holds the same value on every row of a batch. func (, Value) bool { if .kind != .kind { return false } switch .kind { case KindNull: return true case KindBool, KindInt, KindFloat, KindNode, KindRel: return .num == .num case KindTemporal: return .num == .num && .aux == .aux case KindStr: return .str == .str case KindList: , := .ext.list, .ext.list if len() != len() { return false } for := range { if !([], []) { return false } } return true case KindPath: return slicesEqual(.ext.nodes, .ext.nodes) && slicesEqual(.ext.rels, .ext.rels) case KindDuration: return .num == .num && .ext.months == .ext.months && .ext.days == .ext.days case KindMap: , := .ext.entries, .ext.entries if len() != len() { return false } for := range { if [].Key != [].Key || !([].Val, [].Val) { return false } } return true } return false }