// Fingerprint renders a query AST to a canonical string: two ASTs render // identically iff they are structurally identical (every node emits a type // tag with delimited fields, strings are quoted), so the rendering is safe // as a plan-cache template key -- the Go analogue of the Rust engine // keying its L2 on the template AST's debug rendering.
package ast import ( ) // Fingerprint is the canonical rendering of q. func ( *Query) string { var strings.Builder fpQuery(&, ) return .String() } func fpQuery( *strings.Builder, *Query) { .WriteString("Q") .WriteString(strconv.Itoa(int(.Mode))) for , := range .Parts { if > 0 { .WriteString("|U") .WriteString(strconv.Itoa(int(.Union[-1]))) .WriteByte('|') } for := range .Clauses { fpClause(, .Clauses[]) } fpProjection(, &.Ret) } } func fpClause( *strings.Builder, Clause) { switch n := .(type) { case *Match: .WriteString(";M") if .Optional { .WriteByte('?') } if .Acyclic { .WriteByte('a') } if .Repeatable { // REPEATABLE ELEMENTS changes the clause's relationship- // uniqueness semantics; without this byte a walk-mode query // shared a template (and its plan) with its TRAIL twin. .WriteByte('r') } for := range .Patterns { fpPattern(, &.Patterns[]) } fpWhere(, .Where) case *With: .WriteString(";W") fpProjection(, &.Proj) fpWhere(, .Where) case *ShortestPath: .WriteString(";SP") if .Optional { .WriteByte('?') } if .All { .WriteByte('*') } .WriteString(strconv.Quote(.PathVar)) fpPattern(, &.Pattern) if .Weight != nil { .WriteString("w(") .WriteString(strconv.Itoa(int(.Weight.Kind))) .WriteString(strconv.Quote(.Weight.Prop)) .WriteString(strconv.FormatFloat(.Weight.Const, 'g', -1, 64)) fpExpr(, .Weight.Expr) .WriteByte(')') } fpWhere(, .Where) case *CallProc: .WriteString(";C") .WriteString(strconv.Quote(.Proc)) for := range .Args { fpExpr(, .Args[]) } for , := range .Yields { .WriteString("y") .WriteString(strconv.Quote(.Field)) .WriteString(strconv.Quote(.Alias)) } case *PathBind: .WriteString(";PB") if .Optional { .WriteByte('?') } if .Acyclic { .WriteByte('a') } .WriteString(strconv.Quote(.PathVar)) fpPattern(, &.Pattern) fpWhere(, .Where) case *Unwind: .WriteString(";F") .WriteString(strconv.Quote(.Var)) fpExpr(, .Expr) case *CallSubquery: .WriteString(";CS[") for , := range .Imports { .WriteString(strconv.Quote()) } .WriteByte(']') fpQuery(, &.Query) default: // A clause kind with no arm previously wrote NOTHING -- the same // collapse as fpExpr's old "?", one level up. unfingerprintable() } } func fpProjection( *strings.Builder, *Projection) { .WriteString("{P") if .Star { .WriteByte('*') } if .Distinct { .WriteByte('D') } for := range .Items { fpExpr(, .Items[].Expr) .WriteString(strconv.Quote(.Items[].Alias)) } for := range .OrderBy { .WriteString("o") fpExpr(, .OrderBy[].Expr) if .OrderBy[].Desc { .WriteByte('v') } } // OFFSET/LIMIT counts are never auto-lifted, so they belong to the // template identity. if .Skip != nil { .WriteString("s") .WriteString(strconv.FormatUint(*.Skip, 10)) } if .Limit != nil { .WriteString("l") .WriteString(strconv.FormatUint(*.Limit, 10)) } .WriteByte('}') } func fpWhere( *strings.Builder, Expr) { if != nil { .WriteString("w") fpExpr(, ) } } func fpPattern( *strings.Builder, *Pattern) { .WriteString("(") fpNodePat(, &.Start) for := range .Hops { fpRelPat(, &.Hops[].Rel) fpNodePat(, &.Hops[].Node) } .WriteByte(')') } func fpNodePat( *strings.Builder, *NodePat) { .WriteString("n") .WriteString(strconv.Quote(.Var)) for , := range .Labels { .WriteString(strconv.Quote()) } if .LabelExpr != nil { fpLabelExpr(, .LabelExpr) } for := range .Props { .WriteString(strconv.Quote(.Props[].Key)) fpLiteral(, &.Props[].Val) } for := range .PropExprs { .WriteString(strconv.Quote(.PropExprs[].Key)) fpExpr(, .PropExprs[].Val) } if .Where != nil { // Encoded even though desugar clears it before the cache // fingerprints: the key must not depend on pipeline order, and a // dropped row-selecting field would let two different queries // share a plan. .WriteByte('w') fpExpr(, .Where) } } func fpRelPat( *strings.Builder, *RelPat) { .WriteString("r") .WriteString(strconv.Itoa(int(.Dir))) .WriteString(strconv.Quote(.Var)) for , := range .Types { .WriteString(strconv.Quote()) } if .Length != nil { .WriteByte('q') if .Length.Min != nil { .WriteString(strconv.FormatUint(*.Length.Min, 10)) } .WriteByte(',') if .Length.Max != nil { .WriteString(strconv.FormatUint(*.Length.Max, 10)) } } for := range .Props { .WriteString(strconv.Quote(.Props[].Key)) fpLiteral(, &.Props[].Val) } for := range .PropExprs { .WriteString(strconv.Quote(.PropExprs[].Key)) fpExpr(, .PropExprs[].Val) } if .Where != nil { .WriteByte('w') fpExpr(, .Where) } } func fpLabelExpr( *strings.Builder, *LabelExpr) { .WriteString("L") .WriteString(strconv.Itoa(int(.Kind))) .WriteString(strconv.Quote(.Name)) if .L != nil { (, .L) } if .R != nil { (, .R) } .WriteByte('.') } func fpLiteral( *strings.Builder, *Literal) { .WriteString("#") .WriteString(strconv.Itoa(int(.Kind))) switch .Kind { case LitInt: .WriteString(strconv.FormatInt(.I, 10)) case LitFloat: .WriteString(strconv.FormatFloat(.F, 'g', -1, 64)) case LitStr, LitNamedParam: .WriteString(strconv.Quote(.S)) case LitBool: if .B { .WriteByte('t') } else { .WriteByte('f') } case LitParam: .WriteString(strconv.FormatUint(uint64(.P), 10)) } } func fpExpr( *strings.Builder, Expr) { switch n := .(type) { case nil: .WriteString("_") case *Lit: fpLiteral(, &.Value) case *Var: .WriteString("v") .WriteString(strconv.Quote(.Name)) case *Prop: .WriteString("p") .WriteString(strconv.Quote(.Var)) .WriteString(strconv.Quote(.Key)) case *Unary: .WriteString("u") .WriteString(strconv.Itoa(int(.Op))) (, .Expr) case *Binary: .WriteString("b") .WriteString(strconv.Itoa(int(.Op))) (, .LHS) (, .RHS) case *Func: .WriteString("f") .WriteString(strconv.Quote(.Name)) if .Distinct { .WriteByte('D') } if .Star { .WriteByte('*') } for , := range .Args { (, ) } .WriteByte('.') case *ListExpr: .WriteString("[") for , := range .Elems { (, ) } .WriteByte(']') case *In: .WriteString("i") (, .Expr) (, .List) case *IsNull: .WriteString("z") if .Negated { .WriteByte('!') } (, .Expr) case *IsTruth: // Missing cases here are CORRECTNESS bugs, not cosmetics: an expr // node the fingerprint cannot see hashes to the same "?" as any // other, so two different queries share one cached template plan // (found: 1 IS TYPED FLOAT returned 1.5's answer). .WriteString("zt") if .Want { .WriteByte('T') } else { .WriteByte('F') } if .Negated { .WriteByte('!') } (, .Expr) case *IsTyped: .WriteString("zk") .WriteString(strconv.Quote(.Kind)) if .Negated { .WriteByte('!') } (, .Expr) case *Case: .WriteString("c") (, .Operand) for , := range .Whens { (, .Cond) (, .Result) } (, .Else) .WriteByte('.') case *Cost: .WriteString("$") .WriteString(strconv.Quote(.From)) .WriteString(strconv.Quote(.To)) .WriteString(strconv.Itoa(int(.Dir))) for , := range .Types { .WriteString(strconv.Quote()) } .WriteString(strconv.Itoa(int(.Weight.Kind))) .WriteString(strconv.Quote(.Weight.Prop)) .WriteString(strconv.FormatFloat(.Weight.Const, 'g', -1, 64)) (, .Weight.Expr) case *Exists: .WriteString("e") fpPattern(, .Pattern) fpWhere(, .Where) .WriteByte('.') case *CountSub: .WriteString("k") fpPattern(, .Pattern) fpWhere(, .Where) .WriteByte('.') case *ListPred: .WriteString("q") .WriteString(strconv.Itoa(int(.Quant))) .WriteString(strconv.Quote(.Var)) (, .List) (, .Pred) case *Reduce: .WriteString("R") .WriteString(strconv.Quote(.Acc)) (, .Init) .WriteString(strconv.Quote(.Var)) (, .List) (, .Body) case *ListComp: .WriteString("lc") .WriteString(strconv.Quote(.Var)) (, .List) (, .Filter) (, .Map) case *PatternComp: .WriteString("pc") fpPattern(, .Pattern) fpWhere(, .Where) (, .Proj) case *Index: .WriteString("ix") (, .Base) (, .Idx) case *Slice: .WriteString("sl") (, .Base) (, .From) (, .To) case *PropOf: .WriteString("po") (, .Base) .WriteString(strconv.Quote(.Key)) case *MapProj: .WriteString("mp") .WriteString(strconv.Quote(.Var)) for , := range .Entries { .WriteString(strconv.Itoa(int(.Kind))) .WriteString(strconv.Quote(.Key)) (, .Expr) } .WriteByte('.') case *MapLit: .WriteString("ml") for , := range .Fields { .WriteString(strconv.Quote(.Key)) (, .Val) } .WriteByte('.') case *HasLabelExpr: .WriteString("hl") .WriteString(strconv.Quote(.Var)) fpLabelExpr(, .Expr) default: unfingerprintable() } } // unfingerprintable poisons the key for a node kind this walk does not // know. A collapsed constant marker would let two DIFFERENT queries share // a cache key and serve each other's plans -- a wrong answer enabled by a // degenerate fingerprint -- so the marker is unique per occurrence: an // unfingerprintable query simply never hits the cache. The NUL prefix // cannot occur in legitimate fingerprint output (all dynamic strings pass // through strconv.Quote), which is what the kind roll-call test keys on. func unfingerprintable( *strings.Builder) { .WriteString("\x00unfp") .WriteString(strconv.FormatUint(unfpSeq.Add(1), 10)) } var unfpSeq atomic.Uint64