super-graph/qcode/qcode.go

927 lines
16 KiB
Go
Raw Normal View History

2019-03-24 14:57:29 +01:00
package qcode
import (
2019-04-21 01:42:08 +02:00
"errors"
2019-03-24 14:57:29 +01:00
"fmt"
"strings"
"github.com/cespare/xxhash/v2"
2019-03-24 14:57:29 +01:00
"github.com/dosco/super-graph/util"
2019-03-25 05:43:14 +01:00
"github.com/gobuffalo/flect"
2019-03-24 14:57:29 +01:00
)
2019-05-13 01:27:26 +02:00
const (
maxSelectors = 30
)
2019-03-24 14:57:29 +01:00
type QCode struct {
Query *Query
}
type Query struct {
2019-05-13 01:27:26 +02:00
Selects []Select
2019-03-24 14:57:29 +01:00
}
type Column struct {
Table string
Name string
FieldName string
}
type Select struct {
2019-05-13 01:27:26 +02:00
ID uint16
ParentID uint16
RelID uint64
2019-04-07 07:12:11 +02:00
Args map[string]*Node
2019-03-24 14:57:29 +01:00
AsList bool
Table string
Singular string
FieldName string
2019-05-13 01:27:26 +02:00
Cols []Column
2019-03-24 14:57:29 +01:00
Where *Exp
OrderBy []*OrderBy
DistinctOn []string
Paging Paging
2019-05-13 01:27:26 +02:00
Children []uint16
2019-03-24 14:57:29 +01:00
}
type Exp struct {
Op ExpOp
Col string
NestedCol bool
Type ValType
Val string
ListType ValType
ListVal []string
Children []*Exp
}
type OrderBy struct {
Col string
Order Order
}
type Paging struct {
Limit string
Offset string
}
type ExpOp int
const (
2019-04-09 03:24:29 +02:00
OpNop ExpOp = iota
OpAnd
2019-03-24 14:57:29 +01:00
OpOr
OpNot
OpEquals
OpNotEquals
OpGreaterOrEquals
OpLesserOrEquals
OpGreaterThan
OpLesserThan
OpIn
OpNotIn
OpLike
OpNotLike
OpILike
OpNotILike
OpSimilar
OpNotSimilar
OpContains
OpContainedIn
OpHasKey
OpHasKeyAny
OpHasKeyAll
OpIsNull
2019-04-04 06:53:24 +02:00
OpEqID
OpTsQuery
2019-03-24 14:57:29 +01:00
)
type ValType int
const (
ValStr ValType = iota + 1
ValInt
ValFloat
ValBool
ValList
ValVar
ValNone
2019-03-24 14:57:29 +01:00
)
type AggregrateOp int
const (
AgCount AggregrateOp = iota + 1
AgSum
AgAvg
AgMax
AgMin
)
type Order int
const (
OrderAsc Order = iota + 1
OrderDesc
OrderAscNullsFirst
OrderAscNullsLast
OrderDescNullsFirst
OrderDescNullsLast
)
2019-04-08 08:47:59 +02:00
type Config struct {
2019-05-13 01:27:26 +02:00
DefaultFilter []string
FilterMap map[string][]string
Blacklist []string
KeepArgs bool
2019-04-08 08:47:59 +02:00
}
type Compiler struct {
fl *Exp
fm map[string]*Exp
bl map[string]struct{}
ka bool
2019-04-08 08:47:59 +02:00
}
func NewCompiler(c Config) (*Compiler, error) {
bl := make(map[string]struct{}, len(c.Blacklist))
2019-04-08 08:47:59 +02:00
for i := range c.Blacklist {
bl[strings.ToLower(c.Blacklist[i])] = struct{}{}
2019-04-08 08:47:59 +02:00
}
2019-03-24 14:57:29 +01:00
fl, err := compileFilter(c.DefaultFilter)
2019-03-24 14:57:29 +01:00
if err != nil {
return nil, err
}
fm := make(map[string]*Exp, len(c.FilterMap))
2019-03-24 14:57:29 +01:00
for k, v := range c.FilterMap {
2019-04-08 08:47:59 +02:00
fil, err := compileFilter(v)
if err != nil {
return nil, err
}
fm[strings.ToLower(k)] = fil
}
2019-03-24 14:57:29 +01:00
return &Compiler{fl, fm, bl, c.KeepArgs}, nil
2019-03-24 14:57:29 +01:00
}
func (com *Compiler) CompileQuery(query string) (*QCode, error) {
var qc QCode
var err error
op, err := ParseQuery(query)
if err != nil {
return nil, err
}
switch op.Type {
case opQuery:
qc.Query, err = com.compileQuery(op)
case opMutate:
case opSub:
default:
err = fmt.Errorf("Unknown operation type %d", op.Type)
}
if err != nil {
return nil, err
}
opPool.Put(op)
2019-03-24 14:57:29 +01:00
return &qc, nil
}
func (com *Compiler) compileQuery(op *Operation) (*Query, error) {
2019-05-13 01:27:26 +02:00
var id, parentID uint16
2019-03-24 14:57:29 +01:00
2019-05-13 01:27:26 +02:00
selects := make([]Select, 0, 5)
2019-03-24 14:57:29 +01:00
st := util.NewStack()
h := xxhash.New()
2019-03-24 14:57:29 +01:00
2019-05-13 01:27:26 +02:00
if len(op.Fields) == 0 {
return nil, errors.New("empty query")
2019-03-24 14:57:29 +01:00
}
2019-05-13 01:27:26 +02:00
st.Push(op.Fields[0].ID)
2019-03-24 14:57:29 +01:00
for {
if st.Len() == 0 {
break
}
2019-05-13 01:27:26 +02:00
if id >= maxSelectors {
return nil, fmt.Errorf("selector limit reached (%d)", maxSelectors)
}
2019-03-24 14:57:29 +01:00
intf := st.Pop()
2019-05-13 01:27:26 +02:00
fid, ok := intf.(uint16)
2019-03-24 14:57:29 +01:00
2019-05-13 01:27:26 +02:00
if !ok {
return nil, fmt.Errorf("15: unexpected value %v (%t)", intf, intf)
2019-03-24 14:57:29 +01:00
}
2019-05-13 01:27:26 +02:00
field := &op.Fields[fid]
2019-03-24 14:57:29 +01:00
2019-04-07 07:12:11 +02:00
fn := strings.ToLower(field.Name)
if _, ok := com.bl[fn]; ok {
2019-03-24 14:57:29 +01:00
continue
}
2019-03-25 05:43:14 +01:00
tn := flect.Pluralize(fn)
2019-03-24 14:57:29 +01:00
2019-05-13 01:27:26 +02:00
s := Select{
ID: id,
ParentID: parentID,
Table: tn,
Children: make([]uint16, 0, 5),
}
if s.ID != 0 {
p := &selects[s.ParentID]
p.Children = append(p.Children, s.ID)
s.RelID = relID(h, tn, p.Table)
2019-03-25 05:43:14 +01:00
}
if fn == tn {
s.Singular = flect.Singularize(fn)
} else {
s.Singular = fn
2019-03-24 14:57:29 +01:00
}
if fn == s.Table {
s.AsList = true
} else {
s.Paging.Limit = "1"
}
if len(field.Alias) != 0 {
s.FieldName = field.Alias
} else if s.AsList {
s.FieldName = s.Table
} else {
s.FieldName = s.Singular
}
2019-05-13 01:27:26 +02:00
err := com.compileArgs(&s, field.Args)
2019-03-24 14:57:29 +01:00
if err != nil {
return nil, err
}
2019-05-13 01:27:26 +02:00
s.Cols = make([]Column, 0, len(field.Children))
for _, cid := range field.Children {
f := op.Fields[cid]
fn := strings.ToLower(f.Name)
2019-03-24 14:57:29 +01:00
2019-04-07 07:12:11 +02:00
if _, ok := com.bl[fn]; ok {
2019-03-24 14:57:29 +01:00
continue
}
2019-05-13 01:27:26 +02:00
if len(f.Children) != 0 {
parentID = s.ID
st.Push(f.ID)
continue
}
col := Column{Name: fn}
2019-05-13 01:27:26 +02:00
if len(f.Alias) != 0 {
col.FieldName = f.Alias
2019-03-24 14:57:29 +01:00
} else {
2019-05-13 01:27:26 +02:00
col.FieldName = f.Name
2019-03-24 14:57:29 +01:00
}
2019-05-13 01:27:26 +02:00
s.Cols = append(s.Cols, col)
2019-03-24 14:57:29 +01:00
}
2019-05-13 01:27:26 +02:00
selects = append(selects, s)
id++
2019-03-24 14:57:29 +01:00
}
var ok bool
var fil *Exp
2019-05-13 01:27:26 +02:00
if id > 0 {
root := &selects[0]
fil, ok = com.fm[root.Table]
2019-05-13 01:27:26 +02:00
if !ok || fil == nil {
fil = com.fl
}
2019-04-09 03:24:29 +02:00
2019-05-13 01:27:26 +02:00
if fil != nil && fil.Op != OpNop {
if root.Where != nil {
ex := &Exp{Op: OpAnd, Children: []*Exp{fil, root.Where}}
root.Where = ex
} else {
root.Where = fil
}
2019-03-24 14:57:29 +01:00
}
2019-05-13 01:27:26 +02:00
} else {
2019-04-21 01:42:08 +02:00
return nil, errors.New("invalid query")
}
2019-05-13 01:27:26 +02:00
return &Query{selects[:id]}, nil
2019-03-24 14:57:29 +01:00
}
func (com *Compiler) compileArgs(sel *Select, args []Arg) error {
2019-03-24 14:57:29 +01:00
var err error
2019-04-07 07:12:11 +02:00
if com.ka {
sel.Args = make(map[string]*Node, len(args))
}
2019-03-24 14:57:29 +01:00
for i := range args {
arg := &args[i]
an := strings.ToLower(arg.Name)
2019-04-04 06:53:24 +02:00
switch an {
case "id":
2019-05-13 01:27:26 +02:00
if sel.ID == 0 {
err = com.compileArgID(sel, arg)
2019-04-04 06:53:24 +02:00
}
case "search":
err = com.compileArgSearch(sel, arg)
2019-03-24 14:57:29 +01:00
case "where":
err = com.compileArgWhere(sel, arg)
2019-03-24 14:57:29 +01:00
case "orderby", "order_by", "order":
err = com.compileArgOrderBy(sel, arg)
2019-03-24 14:57:29 +01:00
case "distinct_on", "distinct":
err = com.compileArgDistinctOn(sel, arg)
2019-03-24 14:57:29 +01:00
case "limit":
err = com.compileArgLimit(sel, arg)
2019-03-24 14:57:29 +01:00
case "offset":
err = com.compileArgOffset(sel, arg)
2019-03-24 14:57:29 +01:00
}
2019-04-04 06:53:24 +02:00
if err != nil {
return err
}
if sel.Args != nil {
sel.Args[an] = arg.Val
} else {
nodePool.Put(arg.Val)
}
2019-03-24 14:57:29 +01:00
}
2019-04-04 06:53:24 +02:00
return nil
2019-03-24 14:57:29 +01:00
}
type expT struct {
parent *Exp
node *Node
}
func (com *Compiler) compileArgObj(arg *Arg) (*Exp, error) {
if arg.Val.Type != nodeObj {
2019-04-07 07:12:11 +02:00
return nil, fmt.Errorf("expecting an object")
2019-03-24 14:57:29 +01:00
}
return com.compileArgNode(arg.Val)
}
func (com *Compiler) compileArgNode(node *Node) (*Exp, error) {
2019-03-24 14:57:29 +01:00
st := util.NewStack()
var root *Exp
if node == nil || len(node.Children) == 0 {
return nil, errors.New("invalid argument value")
}
st.Push(&expT{nil, node.Children[0]})
2019-03-24 14:57:29 +01:00
for {
if st.Len() == 0 {
break
}
intf := st.Pop()
eT, ok := intf.(*expT)
if !ok || eT == nil {
2019-05-13 01:27:26 +02:00
return nil, fmt.Errorf("16: unexpected value %v (%t)", intf, intf)
2019-03-24 14:57:29 +01:00
}
2019-04-07 07:12:11 +02:00
if len(eT.node.Name) != 0 {
if _, ok := com.bl[strings.ToLower(eT.node.Name)]; ok {
continue
}
2019-03-24 14:57:29 +01:00
}
ex, err := newExp(st, eT)
2019-03-24 14:57:29 +01:00
if err != nil {
return nil, err
2019-03-24 14:57:29 +01:00
}
if ex == nil {
continue
2019-03-24 14:57:29 +01:00
}
if eT.parent == nil {
root = ex
} else {
eT.parent.Children = append(eT.parent.Children, ex)
}
}
if com.ka {
return root, nil
}
st.Push(node.Children[0])
for {
if st.Len() == 0 {
break
}
intf := st.Pop()
node, _ := intf.(*Node)
for i := range node.Children {
st.Push(node.Children[i])
}
nodePool.Put(node)
}
2019-03-24 14:57:29 +01:00
return root, nil
}
2019-04-04 06:53:24 +02:00
func (com *Compiler) compileArgID(sel *Select, arg *Arg) error {
if sel.Where != nil && sel.Where.Op == OpEqID {
return nil
}
ex := &Exp{Op: OpEqID, Val: arg.Val.Val}
switch arg.Val.Type {
case nodeStr:
ex.Type = ValStr
case nodeInt:
ex.Type = ValInt
case nodeFloat:
ex.Type = ValFloat
2019-04-19 07:55:03 +02:00
case nodeVar:
ex.Type = ValVar
2019-04-04 06:53:24 +02:00
default:
2019-04-19 07:55:03 +02:00
fmt.Errorf("expecting a string, int, float or variable")
2019-04-04 06:53:24 +02:00
}
sel.Where = ex
return nil
}
func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) error {
ex := &Exp{
Op: OpTsQuery,
Type: ValStr,
Val: arg.Val.Val,
}
if sel.Where != nil {
sel.Where = &Exp{Op: OpAnd, Children: []*Exp{ex, sel.Where}}
} else {
sel.Where = ex
}
return nil
}
2019-03-24 14:57:29 +01:00
func (com *Compiler) compileArgWhere(sel *Select, arg *Arg) error {
var err error
ex, err := com.compileArgObj(arg)
2019-03-24 14:57:29 +01:00
if err != nil {
return err
}
if sel.Where != nil {
sel.Where = &Exp{Op: OpAnd, Children: []*Exp{ex, sel.Where}}
} else {
sel.Where = ex
}
2019-03-24 14:57:29 +01:00
return nil
}
func (com *Compiler) compileArgOrderBy(sel *Select, arg *Arg) error {
if arg.Val.Type != nodeObj {
return fmt.Errorf("expecting an object")
}
st := util.NewStack()
for i := range arg.Val.Children {
st.Push(arg.Val.Children[i])
}
for {
if st.Len() == 0 {
break
}
intf := st.Pop()
node, ok := intf.(*Node)
if !ok || node == nil {
2019-05-13 01:27:26 +02:00
return fmt.Errorf("17: unexpected value %v (%t)", intf, intf)
2019-03-24 14:57:29 +01:00
}
2019-04-07 07:12:11 +02:00
if _, ok := com.bl[strings.ToLower(node.Name)]; ok {
if !com.ka {
nodePool.Put(node)
}
2019-03-24 14:57:29 +01:00
continue
}
if node.Type == nodeObj {
for i := range node.Children {
st.Push(node.Children[i])
}
if !com.ka {
nodePool.Put(node)
}
2019-03-24 14:57:29 +01:00
continue
}
ob := &OrderBy{}
val := strings.ToLower(node.Val)
switch val {
case "asc":
ob.Order = OrderAsc
case "desc":
ob.Order = OrderDesc
case "asc_nulls_first":
ob.Order = OrderAscNullsFirst
case "desc_nulls_first":
ob.Order = OrderDescNullsFirst
case "asc_nulls_last":
ob.Order = OrderAscNullsLast
case "desc_nulls_last":
ob.Order = OrderDescNullsLast
default:
return fmt.Errorf("valid values include asc, desc, asc_nulls_first and desc_nulls_first")
}
setOrderByColName(ob, node)
sel.OrderBy = append(sel.OrderBy, ob)
if !com.ka {
nodePool.Put(node)
}
2019-03-24 14:57:29 +01:00
}
return nil
}
func (com *Compiler) compileArgDistinctOn(sel *Select, arg *Arg) error {
node := arg.Val
2019-04-07 07:12:11 +02:00
if _, ok := com.bl[strings.ToLower(node.Name)]; ok {
2019-03-24 14:57:29 +01:00
return nil
}
if node.Type != nodeList && node.Type != nodeStr {
return fmt.Errorf("expecting a list of strings or just a string")
}
if node.Type == nodeStr {
sel.DistinctOn = append(sel.DistinctOn, node.Val)
}
for i := range node.Children {
sel.DistinctOn = append(sel.DistinctOn, node.Children[i].Val)
if !com.ka {
nodePool.Put(node.Children[i])
}
2019-03-24 14:57:29 +01:00
}
return nil
}
func (com *Compiler) compileArgLimit(sel *Select, arg *Arg) error {
node := arg.Val
if node.Type != nodeInt {
return fmt.Errorf("expecting an integer")
}
sel.Paging.Limit = node.Val
return nil
}
func (com *Compiler) compileArgOffset(sel *Select, arg *Arg) error {
node := arg.Val
if node.Type != nodeInt {
return fmt.Errorf("expecting an integer")
}
sel.Paging.Offset = node.Val
return nil
}
func compileMutate() (*Query, error) {
return nil, nil
}
func compileSub() (*Query, error) {
return nil, nil
}
func newExp(st *util.Stack, eT *expT) (*Exp, error) {
ex := &Exp{}
node := eT.node
if len(node.Name) == 0 {
pushChildren(st, eT.parent, node)
return nil, nil
}
name := strings.ToLower(node.Name)
if name[0] == '_' {
name = name[1:]
}
switch name {
case "and":
ex.Op = OpAnd
pushChildren(st, ex, node)
case "or":
ex.Op = OpOr
pushChildren(st, ex, node)
case "not":
ex.Op = OpNot
st.Push(&expT{ex, node.Children[0]})
case "eq", "equals":
ex.Op = OpEquals
ex.Val = node.Val
case "neq", "not_equals":
ex.Op = OpNotEquals
ex.Val = node.Val
case "gt", "greater_than":
ex.Op = OpGreaterThan
ex.Val = node.Val
case "lt", "lesser_than":
ex.Op = OpLesserThan
ex.Val = node.Val
case "gte", "greater_or_equals":
ex.Op = OpGreaterOrEquals
ex.Val = node.Val
case "lte", "lesser_or_equals":
ex.Op = OpLesserOrEquals
ex.Val = node.Val
case "in":
ex.Op = OpIn
setListVal(ex, node)
case "nin", "not_in":
ex.Op = OpNotIn
setListVal(ex, node)
case "like":
ex.Op = OpLike
ex.Val = node.Val
case "nlike", "not_like":
ex.Op = OpNotLike
ex.Val = node.Val
case "ilike":
ex.Op = OpILike
ex.Val = node.Val
case "nilike", "not_ilike":
ex.Op = OpILike
ex.Val = node.Val
case "similar":
ex.Op = OpSimilar
ex.Val = node.Val
case "nsimilar", "not_similar":
ex.Op = OpNotSimilar
ex.Val = node.Val
case "contains":
ex.Op = OpContains
ex.Val = node.Val
case "contained_in":
ex.Op = OpContainedIn
ex.Val = node.Val
case "has_key":
ex.Op = OpHasKey
ex.Val = node.Val
case "has_key_any":
ex.Op = OpHasKeyAny
ex.Val = node.Val
case "has_key_all":
ex.Op = OpHasKeyAll
ex.Val = node.Val
case "is_null":
ex.Op = OpIsNull
ex.Val = node.Val
default:
pushChildren(st, eT.parent, node)
return nil, nil // skip node
}
if ex.Op != OpAnd && ex.Op != OpOr && ex.Op != OpNot {
switch node.Type {
case nodeStr:
ex.Type = ValStr
case nodeInt:
ex.Type = ValInt
case nodeBool:
ex.Type = ValBool
case nodeFloat:
ex.Type = ValFloat
case nodeList:
ex.Type = ValList
case nodeVar:
ex.Type = ValVar
default:
return nil, fmt.Errorf("[Where] valid values include string, int, float, boolean and list: %s", node.Type)
}
setWhereColName(ex, node)
}
return ex, nil
}
2019-03-24 14:57:29 +01:00
func setListVal(ex *Exp, node *Node) {
if len(node.Children) != 0 {
switch node.Children[0].Type {
case nodeStr:
ex.ListType = ValStr
case nodeInt:
ex.ListType = ValInt
case nodeBool:
ex.ListType = ValBool
case nodeFloat:
ex.ListType = ValFloat
}
}
for i := range node.Children {
ex.ListVal = append(ex.ListVal, node.Children[i].Val)
}
}
func setWhereColName(ex *Exp, node *Node) {
var list []string
2019-05-13 01:27:26 +02:00
2019-03-24 14:57:29 +01:00
for n := node.Parent; n != nil; n = n.Parent {
if n.Type != nodeObj {
continue
}
2019-05-13 01:27:26 +02:00
if len(n.Name) != 0 {
k := strings.ToLower(n.Name)
if k == "and" || k == "or" || k == "not" ||
k == "_and" || k == "_or" || k == "_not" {
continue
}
2019-03-24 14:57:29 +01:00
list = append([]string{k}, list...)
}
}
if len(list) == 1 {
ex.Col = list[0]
} else if len(list) > 2 {
2019-05-13 01:27:26 +02:00
ex.Col = buildPath(list)
2019-03-24 14:57:29 +01:00
ex.NestedCol = true
}
}
func setOrderByColName(ob *OrderBy, node *Node) {
var list []string
2019-05-13 01:27:26 +02:00
2019-03-24 14:57:29 +01:00
for n := node; n != nil; n = n.Parent {
2019-05-13 01:27:26 +02:00
if len(n.Name) != 0 {
k := strings.ToLower(n.Name)
2019-03-24 14:57:29 +01:00
list = append([]string{k}, list...)
}
}
if len(list) != 0 {
2019-05-13 01:27:26 +02:00
ob.Col = buildPath(list)
2019-03-24 14:57:29 +01:00
}
}
func pushChildren(st *util.Stack, ex *Exp, node *Node) {
for i := range node.Children {
st.Push(&expT{ex, node.Children[i]})
}
}
2019-04-08 08:47:59 +02:00
func compileFilter(filter []string) (*Exp, error) {
var fl *Exp
com := &Compiler{}
2019-04-09 03:24:29 +02:00
if len(filter) == 0 {
return &Exp{Op: OpNop}, nil
}
2019-04-08 08:47:59 +02:00
for i := range filter {
node, err := ParseArgValue(filter[i])
if err != nil {
return nil, err
}
f, err := com.compileArgNode(node)
if err != nil {
return nil, err
}
if fl == nil {
fl = f
} else {
fl = &Exp{Op: OpAnd, Children: []*Exp{fl, f}}
}
}
return fl, nil
}
2019-05-13 01:27:26 +02:00
func buildPath(a []string) string {
switch len(a) {
case 0:
return ""
case 1:
return a[0]
}
n := len(a) - 1
for i := 0; i < len(a); i++ {
n += len(a[i])
}
var b strings.Builder
b.Grow(n)
b.WriteString(a[0])
for _, s := range a[1:] {
b.WriteRune('.')
b.WriteString(s)
}
return b.String()
}
func relID(h *xxhash.Digest, child, parent string) uint64 {
h.WriteString(child)
h.WriteString(parent)
v := h.Sum64()
h.Reset()
return v
}
func (t ExpOp) String() string {
var v string
switch t {
case OpNop:
v = "op-nop"
case OpAnd:
v = "op-and"
case OpOr:
v = "op-or"
case OpNot:
v = "op-not"
case OpEquals:
v = "op-equals"
case OpNotEquals:
v = "op-not-equals"
case OpGreaterOrEquals:
v = "op-greater-or-equals"
case OpLesserOrEquals:
v = "op-lesser-or-equals"
case OpGreaterThan:
v = "op-greater-than"
case OpLesserThan:
v = "op-lesser-than"
case OpIn:
v = "op-in"
case OpNotIn:
v = "op-not-in"
case OpLike:
v = "op-like"
case OpNotLike:
v = "op-not-like"
case OpILike:
v = "op-i-like"
case OpNotILike:
v = "op-not-i-like"
case OpSimilar:
v = "op-similar"
case OpNotSimilar:
v = "op-not-similar"
case OpContains:
v = "op-contains"
case OpContainedIn:
v = "op-contained-in"
case OpHasKey:
v = "op-has-key"
case OpHasKeyAny:
v = "op-has-key-any"
case OpHasKeyAll:
v = "op-has-key-all"
case OpIsNull:
v = "op-is-null"
case OpEqID:
v = "op-eq-id"
case OpTsQuery:
v = "op-ts-query"
}
return fmt.Sprintf("<%s>", v)
}