diff --git a/pkg/sql/opt/build.go b/pkg/sql/opt/build.go new file mode 100644 index 000000000000..63b7daa16f05 --- /dev/null +++ b/pkg/sql/opt/build.go @@ -0,0 +1,163 @@ +// Copyright 2017 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package opt + +import ( + "fmt" + + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" +) + +// Map from tree.ComparisonOperator to operator. +var comparisonOpMap = [...]operator{ + tree.EQ: eqOp, + tree.LT: ltOp, + tree.GT: gtOp, + tree.LE: leOp, + tree.GE: geOp, + tree.NE: neOp, + tree.In: inOp, + tree.NotIn: notInOp, + tree.Like: likeOp, + tree.NotLike: notLikeOp, + tree.ILike: iLikeOp, + tree.NotILike: notILikeOp, + tree.SimilarTo: similarToOp, + tree.NotSimilarTo: notSimilarToOp, + tree.RegMatch: regMatchOp, + tree.NotRegMatch: notRegMatchOp, + tree.RegIMatch: regIMatchOp, + tree.NotRegIMatch: notRegIMatchOp, + tree.IsDistinctFrom: isDistinctFromOp, + tree.IsNotDistinctFrom: isNotDistinctFromOp, + tree.Is: isOp, + tree.IsNot: isNotOp, + tree.Any: anyOp, + tree.Some: someOp, + tree.All: allOp, +} + +// Map from tree.BinaryOperator to operator. +var binaryOpMap = [...]operator{ + tree.Bitand: bitandOp, + tree.Bitor: bitorOp, + tree.Bitxor: bitxorOp, + tree.Plus: plusOp, + tree.Minus: minusOp, + tree.Mult: multOp, + tree.Div: divOp, + tree.FloorDiv: floorDivOp, + tree.Mod: modOp, + tree.Pow: powOp, + tree.Concat: concatOp, + tree.LShift: lShiftOp, + tree.RShift: rShiftOp, +} + +// Map from tree.UnaryOperator to operator. +var unaryOpMap = [...]operator{ + tree.UnaryPlus: unaryPlusOp, + tree.UnaryMinus: unaryMinusOp, + tree.UnaryComplement: unaryComplementOp, +} + +// We allocate *scalarProps and *expr in chunks of these sizes. +const exprAllocChunk = 16 +const scalarPropsAllocChunk = 16 + +type buildContext struct { + preallocScalarProps []scalarProps + preallocExprs []expr +} + +func (bc *buildContext) newScalarProps() *scalarProps { + if len(bc.preallocScalarProps) == 0 { + bc.preallocScalarProps = make([]scalarProps, scalarPropsAllocChunk) + } + p := &bc.preallocScalarProps[0] + bc.preallocScalarProps = bc.preallocScalarProps[1:] + return p +} + +// newExpr returns a new *expr with a new, blank scalarProps. +func (bc *buildContext) newExpr() *expr { + if len(bc.preallocExprs) == 0 { + bc.preallocExprs = make([]expr, exprAllocChunk) + } + e := &bc.preallocExprs[0] + bc.preallocExprs = bc.preallocExprs[1:] + e.scalarProps = bc.newScalarProps() + return e +} + +// buildScalar converts a tree.TypedExpr to an expr tree. +func buildScalar(buildCtx *buildContext, pexpr tree.TypedExpr) *expr { + switch t := pexpr.(type) { + case *tree.ParenExpr: + return buildScalar(buildCtx, t.TypedInnerExpr()) + } + + e := buildCtx.newExpr() + e.scalarProps.typ = pexpr.ResolvedType() + + switch t := pexpr.(type) { + case *tree.AndExpr: + initBinaryExpr( + e, andOp, + buildScalar(buildCtx, t.TypedLeft()), + buildScalar(buildCtx, t.TypedRight()), + ) + case *tree.OrExpr: + initBinaryExpr( + e, orOp, + buildScalar(buildCtx, t.TypedLeft()), + buildScalar(buildCtx, t.TypedRight()), + ) + case *tree.NotExpr: + initUnaryExpr(e, notOp, buildScalar(buildCtx, t.TypedInnerExpr())) + + case *tree.BinaryExpr: + initBinaryExpr( + e, binaryOpMap[t.Operator], + buildScalar(buildCtx, t.TypedLeft()), + buildScalar(buildCtx, t.TypedRight()), + ) + case *tree.ComparisonExpr: + initBinaryExpr( + e, comparisonOpMap[t.Operator], + buildScalar(buildCtx, t.TypedLeft()), + buildScalar(buildCtx, t.TypedRight()), + ) + case *tree.UnaryExpr: + initUnaryExpr(e, unaryOpMap[t.Operator], buildScalar(buildCtx, t.TypedInnerExpr())) + + case *tree.FuncExpr: + children := make([]*expr, len(t.Exprs)) + for i, pexpr := range t.Exprs { + children[i] = buildScalar(buildCtx, pexpr.(tree.TypedExpr)) + } + initFunctionCallExpr(e, t.ResolvedFunc(), children) + + case *tree.IndexedVar: + initVariableExpr(e, t.Idx) + + case tree.Datum: + initConstExpr(e, t) + + default: + panic(fmt.Sprintf("node %T not supported", t)) + } + return e +} diff --git a/pkg/sql/opt/expr.go b/pkg/sql/opt/expr.go new file mode 100644 index 000000000000..232d89a66db8 --- /dev/null +++ b/pkg/sql/opt/expr.go @@ -0,0 +1,74 @@ +// Copyright 2017 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package opt + +import "github.com/cockroachdb/cockroach/pkg/util/treeprinter" + +// expr implements the node of a unified expressions tree for both relational +// and scalar expressions in a query. +// +// Expressions have optional inputs. Expressions also maintain properties; the +// types of properties depend on the expression type (or equivalently, operator +// type). For scalar expressions, the properties are stored in scalarProps. An +// example of a scalar property is the type (types.T) of the scalar expression. +// +// Currently, expr only supports scalar expressions and operators. More +// information pertaining to relational operators will be added when they are +// supported. +// +// TODO(radu): support relational operators and extend this description. +type expr struct { + op operator + // Child expressions. The interpretation of the children is operator + // dependent. For example, for a eqOp, there are two child expressions (the + // left-hand side and the right-hand side); for an andOp, there are at least + // two child expressions (each one being a conjunct). + children []*expr + // Scalar properties (properties that pertain only to scalar operators). + scalarProps *scalarProps + // Operator-dependent data used by this expression. For example, constOp + // stores a pointer to the constant value. + private interface{} +} + +func (e *expr) opClass() operatorClass { + return operatorTab[e.op].class +} + +func (e *expr) inputs() []*expr { + return e.children +} + +func formatExprs(tp treeprinter.Node, title string, exprs []*expr) { + if len(exprs) > 0 { + n := tp.Child(title) + for _, e := range exprs { + if e != nil { + e.format(n) + } + } + } +} + +// format is part of the operatorClass interface. +func (e *expr) format(tp treeprinter.Node) { + e.opClass().format(e, tp) +} + +func (e *expr) String() string { + tp := treeprinter.New() + e.format(tp) + return tp.String() +} diff --git a/pkg/sql/opt/operator.go b/pkg/sql/opt/operator.go new file mode 100644 index 000000000000..a1fcd7f8fbd7 --- /dev/null +++ b/pkg/sql/opt/operator.go @@ -0,0 +1,132 @@ +// Copyright 2017 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package opt + +import ( + "fmt" + + "github.com/cockroachdb/cockroach/pkg/util/treeprinter" +) + +type operator uint8 + +const ( + unknownOp operator = iota + + // TODO(radu): no relational operators yet. + + // -- Scalar operators -- + + // variableOp is a leaf expression that represents a non-constant value, like a column + // in a table. + variableOp + + // constOp is a leaf expression that has a constant value. + constOp + + // listOp is an unordered list of expressions. Currently unused. + listOp + + // orderedListOp is an ordered list of expressions. Currently unused. + orderedListOp + + andOp + orOp + notOp + + eqOp + ltOp + gtOp + leOp + geOp + neOp + inOp + notInOp + likeOp + notLikeOp + iLikeOp + notILikeOp + similarToOp + notSimilarToOp + regMatchOp + notRegMatchOp + regIMatchOp + notRegIMatchOp + isDistinctFromOp + isNotDistinctFromOp + isOp + isNotOp + anyOp + someOp + allOp + + bitandOp + bitorOp + bitxorOp + plusOp + minusOp + multOp + divOp + floorDivOp + modOp + powOp + concatOp + lShiftOp + rShiftOp + + unaryPlusOp + unaryMinusOp + unaryComplementOp + + functionCallOp + + // This should be last. + numOperators +) + +// operatorInfo stores static information about an operator. +type operatorInfo struct { + // name of the operator, used when printing expressions. + name string + // class of the operator (see operatorClass). + class operatorClass +} + +// operatorTab stores static information about all operators. +var operatorTab = [numOperators]operatorInfo{ + unknownOp: {name: "unknown"}, +} + +func (op operator) String() string { + if op >= numOperators { + return fmt.Sprintf("operator(%d)", op) + } + return operatorTab[op].name +} + +// registerOperator initializes the operator's entry in operatorTab. +// There must be a call to registerOperator in an init() function for every +// operator. +func registerOperator(op operator, name string, class operatorClass) { + operatorTab[op].name = name + operatorTab[op].class = class +} + +// operatorClass implements functionality that is common for a subset of +// operators. +type operatorClass interface { + // format outputs information about the expr tree to a treePrinter. + format(e *expr, tp treeprinter.Node) +} diff --git a/pkg/sql/opt/opt_test.go b/pkg/sql/opt/opt_test.go new file mode 100644 index 000000000000..e20684c4d179 --- /dev/null +++ b/pkg/sql/opt/opt_test.go @@ -0,0 +1,304 @@ +// Copyright 2017 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package opt + +// This file is home to TestOpt, which is similar to the logic tests, except it +// is used for optimizer-specific testcases. +// +// Each testfile contains testcases of the form +// +// +// ---- +// +// +// The supported commands are: +// +// - build-scalar [ ...] +// +// Builds an expression tree from a scalar SQL expression and outputs a +// representation of the tree. The expression can refer to external variables +// using @1, @2, etc. in which case the types of the variables must be passed +// on the command line. + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/cockroachdb/cockroach/pkg/sql/coltypes" + "github.com/cockroachdb/cockroach/pkg/sql/parser" + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/sql/sem/types" +) + +var ( + logicTestData = flag.String("d", "testdata/[^.]*", "test data glob") + rewriteTestFiles = flag.Bool( + "rewrite", false, + "ignore the expected results and rewrite the test files with the actual results from this "+ + "run. Used to update tests when a change affects many cases; please verify the testfile "+ + "diffs carefully!", + ) +) + +type lineScanner struct { + *bufio.Scanner + line int +} + +func newLineScanner(r io.Reader) *lineScanner { + return &lineScanner{ + Scanner: bufio.NewScanner(r), + line: 0, + } +} + +func (l *lineScanner) Scan() bool { + ok := l.Scanner.Scan() + if ok { + l.line++ + } + return ok +} + +type testdata struct { + pos string // file and line number + cmd string + cmdArgs []string + sql string + expected string +} + +func (td testdata) fatalf(t *testing.T, format string, args ...interface{}) { + t.Helper() + t.Fatalf("%s: %s", td.pos, fmt.Sprintf(format, args...)) +} + +type testdataReader struct { + path string + file *os.File + scanner *lineScanner + data testdata + rewrite *bytes.Buffer +} + +func newTestdataReader(t *testing.T, path string) *testdataReader { + t.Helper() + + file, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + var rewrite *bytes.Buffer + if *rewriteTestFiles { + rewrite = &bytes.Buffer{} + } + return &testdataReader{ + path: path, + file: file, + scanner: newLineScanner(file), + rewrite: rewrite, + } +} + +func (r *testdataReader) Close() error { + return r.file.Close() +} + +func (r *testdataReader) Next(t *testing.T) bool { + t.Helper() + + r.data = testdata{} + for r.scanner.Scan() { + line := r.scanner.Text() + r.emit(line) + + fields := strings.Fields(line) + if len(fields) == 0 { + continue + } + cmd := fields[0] + if strings.HasPrefix(cmd, "#") { + // Skip comment lines. + continue + } + r.data.pos = fmt.Sprintf("%s:%d", r.path, r.scanner.line) + r.data.cmd = cmd + r.data.cmdArgs = fields[1:] + + var buf bytes.Buffer + var separator bool + for r.scanner.Scan() { + line := r.scanner.Text() + if strings.TrimSpace(line) == "" { + break + } + + r.emit(line) + if line == "----" { + separator = true + break + } + fmt.Fprintln(&buf, line) + } + + r.data.sql = strings.TrimSpace(buf.String()) + + if separator { + buf.Reset() + for r.scanner.Scan() { + line := r.scanner.Text() + if strings.TrimSpace(line) == "" { + break + } + fmt.Fprintln(&buf, line) + } + r.data.expected = buf.String() + } + return true + } + return false +} + +func (r *testdataReader) emit(s string) { + if r.rewrite != nil { + r.rewrite.WriteString(s) + r.rewrite.WriteString("\n") + } +} + +// runTest reads through a file; for every testcase, it breaks it up into +// testdata.cmd, testdata.sql, testdata.expected, calls f, then compares the +// results. +func runTest(t *testing.T, path string, f func(d *testdata) string) { + r := newTestdataReader(t, path) + for r.Next(t) { + d := &r.data + str := f(d) + if r.rewrite != nil { + r.emit(str) + } else if d.expected != str { + t.Fatalf("%s: %s\nexpected:\n%s\nfound:\n%s", d.pos, d.sql, d.expected, str) + } else if testing.Verbose() { + fmt.Printf("%s:\n%s\n----\n%s", d.pos, d.sql, str) + } + } + + if r.rewrite != nil { + data := r.rewrite.Bytes() + if l := len(data); l > 2 && data[l-1] == '\n' && data[l-2] == '\n' { + data = data[:l-1] + } + err := ioutil.WriteFile(path, data, 0644) + if err != nil { + t.Fatal(err) + } + } +} + +func TestOpt(t *testing.T) { + paths, err := filepath.Glob(*logicTestData) + if err != nil { + t.Fatal(err) + } + if len(paths) == 0 { + t.Fatalf("no testfiles found matching: %s", *logicTestData) + } + + for _, path := range paths { + t.Run(filepath.Base(path), func(t *testing.T) { + runTest(t, path, func(d *testdata) string { + switch d.cmd { + case "build-scalar": + typedExpr, err := parseScalarExpr(d.sql, d.cmdArgs) + if err != nil { + d.fatalf(t, "%v", err) + } + + e := func() *expr { + defer func() { + if r := recover(); r != nil { + d.fatalf(t, "buildScalar: %v", r) + } + }() + return buildScalar(&buildContext{}, typedExpr) + }() + return e.String() + default: + d.fatalf(t, "unsupported command: %s", d.cmd) + return "" + } + }) + }) + } +} + +func parseType(typeStr string) (types.T, error) { + colType, err := parser.ParseType(typeStr) + if err != nil { + return nil, err + } + return coltypes.CastTargetToDatumType(colType), nil +} + +type indexedVars struct { + types []types.T +} + +var _ tree.IndexedVarContainer = &indexedVars{} + +func (*indexedVars) IndexedVarEval(idx int, ctx *tree.EvalContext) (tree.Datum, error) { + panic("unimplemented") +} + +func (iv *indexedVars) IndexedVarResolvedType(idx int) types.T { + return iv.types[idx] +} + +func (*indexedVars) IndexedVarNodeFormatter(idx int) tree.NodeFormatter { + panic("unimplemented") +} + +func parseScalarExpr(sql string, indexVarTypes []string) (tree.TypedExpr, error) { + expr, err := parser.ParseExpr(sql) + if err != nil { + return nil, err + } + + // Set up an indexed var helper so we can type-check the expression. + iv := &indexedVars{ + types: make([]types.T, len(indexVarTypes)), + } + for i, typeStr := range indexVarTypes { + var err error + iv.types[i], err = parseType(typeStr) + if err != nil { + return nil, err + } + } + + sema := tree.MakeSemaContext(false /* privileged */) + iVarHelper := tree.MakeIndexedVarHelper(iv, len(iv.types)) + sema.IVarHelper = &iVarHelper + + return expr.TypeCheck(&sema, types.Any) +} diff --git a/pkg/sql/opt/scalar.go b/pkg/sql/opt/scalar.go new file mode 100644 index 000000000000..ad07e7fe9bd9 --- /dev/null +++ b/pkg/sql/opt/scalar.go @@ -0,0 +1,139 @@ +// Copyright 2017 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package opt + +import ( + "bytes" + "fmt" + + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/sql/sem/types" + "github.com/cockroachdb/cockroach/pkg/util/treeprinter" +) + +func init() { + scalarOpNames := map[operator]string{ + variableOp: "variable", + constOp: "const", + listOp: "list", + orderedListOp: "ordered-list", + andOp: "and", + orOp: "or", + notOp: "not", + eqOp: "eq", + ltOp: "lt", + gtOp: "gt", + leOp: "le", + geOp: "ge", + neOp: "ne", + inOp: "in", + notInOp: "not-in", + likeOp: "like", + notLikeOp: "not-like", + iLikeOp: "ilike", + notILikeOp: "not-ilike", + similarToOp: "similar-to", + notSimilarToOp: "not-similar-to", + regMatchOp: "regmatch", + notRegMatchOp: "not-regmatch", + regIMatchOp: "regimatch", + notRegIMatchOp: "not-regimatch", + isDistinctFromOp: "is-distinct-from", + isNotDistinctFromOp: "is-not-distinct-from", + isOp: "is", + isNotOp: "is-not", + anyOp: "any", + someOp: "some", + allOp: "all", + bitandOp: "bitand", + bitorOp: "bitor", + bitxorOp: "bitxor", + plusOp: "plus", + minusOp: "minus", + multOp: "mult", + divOp: "div", + floorDivOp: "floor-div", + modOp: "mod", + powOp: "pow", + concatOp: "concat", + lShiftOp: "lshift", + rShiftOp: "rshift", + unaryPlusOp: "unary-plus", + unaryMinusOp: "unary-minus", + unaryComplementOp: "complement", + functionCallOp: "func", + } + + for op, name := range scalarOpNames { + registerOperator(op, name, scalarClass{}) + } +} + +// scalarProps are properties specific to scalar expressions. An instance of +// scalarProps is associated with an expr node with a scalar operator. +type scalarProps struct { + // typ is the semantic type of the scalar expression. + typ types.T +} + +type scalarClass struct{} + +var _ operatorClass = scalarClass{} + +func (scalarClass) format(e *expr, tp treeprinter.Node) { + var buf bytes.Buffer + fmt.Fprintf(&buf, "%v", e.op) + if e.private != nil { + fmt.Fprintf(&buf, " (%v)", e.private) + } + fmt.Fprintf(&buf, " (type: %s)", e.scalarProps.typ) + n := tp.Child(buf.String()) + formatExprs(n, "inputs", e.inputs()) +} + +// The following initializers are called on an already allocated expression +// node. The scalarProps must be initialized separately. + +// initConstExpr initializes a constOp expression node. +func initConstExpr(e *expr, datum tree.Datum) { + e.op = constOp + e.private = datum +} + +// initFunctionCallExpr initializes a functionCallOp expression node. +func initFunctionCallExpr(e *expr, def *tree.FunctionDefinition, children []*expr) { + e.op = functionCallOp + e.children = children + e.private = def +} + +// initUnaryExpr initializes expression nodes for operators with a single input. +func initUnaryExpr(e *expr, op operator, input *expr) { + e.op = op + e.children = []*expr{input} +} + +// initBinaryExpr initializes expression nodes for operators with two inputs. +func initBinaryExpr(e *expr, op operator, input1 *expr, input2 *expr) { + e.op = op + e.children = []*expr{input1, input2} +} + +// initVariableExpr initializes a variableOp expression node. +// The index refers to a column index (currently derived from an IndexedVar). +func initVariableExpr(e *expr, index int) { + e.op = variableOp + e.private = index +} diff --git a/pkg/sql/opt/testdata/build-scalar b/pkg/sql/opt/testdata/build-scalar new file mode 100644 index 000000000000..7cb571b80719 --- /dev/null +++ b/pkg/sql/opt/testdata/build-scalar @@ -0,0 +1,45 @@ +build-scalar +1 +---- +const (1) (type: int) + +build-scalar +1 + 2 +---- +plus (type: int) + └── inputs + ├── const (1) (type: int) + └── const (2) (type: int) + +build-scalar string +@1 +---- +variable (0) (type: string) + +build-scalar int +@1 + 2 +---- +plus (type: int) + └── inputs + ├── variable (0) (type: int) + └── const (2) (type: int) + +build-scalar int int +@1 >= 5 AND @1 <= 10 AND @2 < 4 +---- +and (type: bool) + └── inputs + ├── and (type: bool) + │ └── inputs + │ ├── ge (type: bool) + │ │ └── inputs + │ │ ├── variable (0) (type: int) + │ │ └── const (5) (type: int) + │ └── le (type: bool) + │ └── inputs + │ ├── variable (0) (type: int) + │ └── const (10) (type: int) + └── lt (type: bool) + └── inputs + ├── variable (1) (type: int) + └── const (4) (type: int) diff --git a/pkg/sql/sem/tree/expr.go b/pkg/sql/sem/tree/expr.go index b96574d4c1da..bcf34de33128 100644 --- a/pkg/sql/sem/tree/expr.go +++ b/pkg/sql/sem/tree/expr.go @@ -945,6 +945,12 @@ type FuncExpr struct { fn Builtin } +// ResolvedFunc returns the function definition; can only be called after +// Resolve (which happens during TypeCheck). +func (node *FuncExpr) ResolvedFunc() *FunctionDefinition { + return node.Func.FunctionReference.(*FunctionDefinition) +} + // GetAggregateConstructor exposes the AggregateFunc field for use by // the group node in package sql. func (node *FuncExpr) GetAggregateConstructor() func(*EvalContext) AggregateFunc {