Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

opt: build UDF expressions #84723

Merged
merged 2 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkg/sql/opt/memo/expr_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,9 @@ func FormatPrivate(f *ExprFmtCtx, private interface{}, physProps *physical.Requi
case *FunctionPrivate:
fmt.Fprintf(f.Buffer, " %s", t.Name)

case *UserDefinedFunctionPrivate:
fmt.Fprintf(f.Buffer, " %s", t.Name)

case *WindowsItemPrivate:
fmt.Fprintf(f.Buffer, " frame=%q", &t.Frame)

Expand Down
4 changes: 4 additions & 0 deletions pkg/sql/opt/norm/decorrelate_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ func (c *CustomFuncs) deriveHasHoistableSubquery(scalar opt.ScalarExpr) bool {
// WHERE clause, it will be transformed to an Exists operator, so this case
// only occurs when the Any is nested, in a projection, etc.
return !t.Input.Relational().OuterCols.Empty()

case *memo.UserDefinedFunctionExpr:
// Do not attempt to hoist UDFs.
return false
}

// If HasHoistableSubquery is true for any child, then it's true for this
Expand Down
21 changes: 21 additions & 0 deletions pkg/sql/opt/norm/testdata/rules/udf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
exec-ddl
CREATE FUNCTION one() RETURNS INT LANGUAGE SQL AS 'SELECT 1';
----

# Do not attempt to hoist UDFs.
norm
SELECT one()
----
values
├── columns: one:2
├── cardinality: [1 - 1]
├── key: ()
├── fd: ()-->(2)
└── tuple
└── user-defined-function: one
└── values
├── columns: "?column?":1!null
├── cardinality: [1 - 1]
├── key: ()
├── fd: ()-->(1)
└── (1,)
15 changes: 15 additions & 0 deletions pkg/sql/opt/ops/scalar.opt
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,21 @@ define NthValue {
Nth ScalarExpr
}

# UserDefinedFunction invokes a user-defined function. The
# UserDefinedFunctionPrivate field contains the name of the function and a
# pointer to its type.
[Scalar]
define UserDefinedFunction {
Body RelExpr
_ UserDefinedFunctionPrivate
}

[Private]
define UserDefinedFunctionPrivate {
Name string
Typ Type
}

# KVOptions is a set of KVOptionItems that specify arbitrary keys and values
# that are used as modifiers for various statements (see tree.KVOptions). The
# key is a constant string but the value can be a scalar expression.
Expand Down
34 changes: 34 additions & 0 deletions pkg/sql/opt/optbuilder/scalar.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/sql/opt/cat"
"github.com/cockroachdb/cockroach/pkg/sql/opt/memo"
"github.com/cockroachdb/cockroach/pkg/sql/opt/norm"
"github.com/cockroachdb/cockroach/pkg/sql/parser"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
"github.com/cockroachdb/cockroach/pkg/sql/privilege"
Expand Down Expand Up @@ -529,6 +530,10 @@ func (b *Builder) buildFunction(
panic(err)
}

if f.ResolvedOverload().Body != "" {
return b.buildUDF(f, def, inScope, outScope, outCol)
}

if isAggregate(def) {
panic(errors.AssertionFailedf("aggregate function should have been replaced"))
}
Expand Down Expand Up @@ -583,6 +588,35 @@ func (b *Builder) buildFunction(
return b.finishBuildScalar(f, out, inScope, outScope, outCol)
}

// buildUDF builds a set of memo groups that represents a user-defined function
// invocation.
// TODO(mgartner): Support multi-statement UDFs.
// TODO(mgartner): Support UDFs with arguments.
func (b *Builder) buildUDF(
f *tree.FuncExpr, def *tree.FunctionDefinition, inScope, outScope *scope, outCol *scopeColumn,
) (out opt.ScalarExpr) {
stmt, err := parser.ParseOne(f.ResolvedOverload().Body)
if err != nil {
panic(err)
}

// A statement inside a UDF body cannot refer to anything from the outer
// expression calling the function, so we use an empty scope.
// TODO(mgartner): We may need to set bodyScope.atRoot=true to prevent CTEs
// that mutate and are not at the top-level.
bodyScope := b.allocScope()
bodyScope = b.buildStmt(stmt.AST, nil /* desiredTypes */, bodyScope)

out = b.factory.ConstructUserDefinedFunction(
bodyScope.expr,
&memo.UserDefinedFunctionPrivate{
Name: def.Name,
Typ: f.ResolvedType(),
},
)
return b.finishBuildScalar(f, out, inScope, outScope, outCol)
}

// buildRangeCond builds a RANGE clause as a simpler expression. Examples:
// x BETWEEN a AND b -> x >= a AND x <= b
// x NOT BETWEEN a AND b -> NOT (x >= a AND x <= b)
Expand Down
68 changes: 68 additions & 0 deletions pkg/sql/opt/optbuilder/testdata/udf
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
exec-ddl
CREATE TABLE abc (
a INT PRIMARY KEY,
b INT,
c INT
)
----

build
SELECT foo()
----
error (42883): unknown function: foo

exec-ddl
CREATE FUNCTION one() RETURNS INT LANGUAGE SQL AS 'SELECT 1';
----

build
SELECT one()
----
project
├── columns: one:2
├── values
│ └── ()
└── projections
└── user-defined-function: one [as=one:2]
└── project
├── columns: "?column?":1!null
├── values
│ └── ()
└── projections
└── 1 [as="?column?":1]

build
SELECT *, one() FROM abc
----
project
├── columns: a:1!null b:2 c:3 one:7
├── scan abc
│ └── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5
└── projections
└── user-defined-function: one [as=one:7]
└── project
├── columns: "?column?":6!null
├── values
│ └── ()
└── projections
└── 1 [as="?column?":6]

build
SELECT * FROM abc WHERE one() = c
----
project
├── columns: a:1!null b:2 c:3
└── select
├── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5
├── scan abc
│ └── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5
└── filters
└── eq
├── user-defined-function: one
│ └── project
│ ├── columns: "?column?":6!null
│ ├── values
│ │ └── ()
│ └── projections
│ └── 1 [as="?column?":6]
└── c:3
1 change: 1 addition & 0 deletions pkg/sql/opt/testutils/opttester/opt_tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ func New(catalog cat.Catalog, sql string) *OptTester {
semaCtx: tree.MakeSemaContext(),
evalCtx: eval.MakeTestingEvalContext(cluster.MakeTestingClusterSettings()),
}
ot.semaCtx.SearchPath = tree.EmptySearchPath
ot.semaCtx.FunctionResolver = ot.catalog
// To allow opttester tests to use now(), we hardcode a preset transaction
// time. May 10, 2017 is a historic day: the release date of CockroachDB 1.0.
Expand Down