diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md index 6cf96ce88c52..08d0973dbeb4 100644 --- a/docs/generated/sql/functions.md +++ b/docs/generated/sql/functions.md @@ -991,6 +991,9 @@ SELECT * FROM crdb_internal.check_consistency(true, ‘\x02’, ‘\x04’)

current_user() → string

Returns the current user. This function is provided for compatibility with PostgreSQL.

+locality() → tuple

Returns the hierarchical location of the current node as a tuple of labeled values, ordered from most inclusive to least inclusive.

+

For example: region=east,datacenter=us-east-1.

+
version() → string

Returns the node’s version of CockroachDB.

diff --git a/pkg/sql/conn_executor.go b/pkg/sql/conn_executor.go index dbcbb59e6509..30630a44b2a6 100644 --- a/pkg/sql/conn_executor.go +++ b/pkg/sql/conn_executor.go @@ -1877,6 +1877,7 @@ func (ex *connExecutor) resetPlanner( p.semaCtx.Location = &ex.sessionData.DataConversion.Location p.semaCtx.SearchPath = ex.sessionData.SearchPath p.semaCtx.AsOfTimestamp = nil + p.semaCtx.Locality = ex.server.cfg.Locality ex.resetEvalCtx(&p.extendedEvalCtx, txn, stmtTS) diff --git a/pkg/sql/logictest/testdata/logic_test/locality b/pkg/sql/logictest/testdata/logic_test/locality new file mode 100644 index 000000000000..49854a96701d --- /dev/null +++ b/pkg/sql/logictest/testdata/logic_test/locality @@ -0,0 +1,19 @@ +# LogicTest: 5node-dist 5node-dist-opt + +query T +SELECT locality() +---- +(test,dc1) + +query T +SELECT (locality()).region +---- +test + +query T +SELECT (locality()).dc +---- +dc1 + +statement error could not identify column "unk" in tuple{string AS region, string AS dc} +SELECT (locality()).unk diff --git a/pkg/sql/opt/memo/typing.go b/pkg/sql/opt/memo/typing.go index a1643980564f..4d60ff4d1d56 100644 --- a/pkg/sql/opt/memo/typing.go +++ b/pkg/sql/opt/memo/typing.go @@ -296,7 +296,7 @@ func typeAsAggregate(e opt.ScalarExpr) *types.T { // types (i.e. pass nil to the ReturnTyper). Aggregates with return types // that depend on argument types are handled separately. _, overload := FindAggregateOverload(e) - t := overload.ReturnType(nil) + t := overload.ReturnType(nil /* ctx */, nil /* args */) if t == tree.UnknownReturnType { panic(pgerror.AssertionFailedf("unknown aggregate return type. e:\n%s", e)) } @@ -307,7 +307,7 @@ func typeAsAggregate(e opt.ScalarExpr) *types.T { // typeAsAggregate. func typeAsWindow(e opt.ScalarExpr) *types.T { _, overload := FindWindowOverload(e) - t := overload.ReturnType(nil) + t := overload.ReturnType(nil /* ctx */, nil /* args */) if t == tree.UnknownReturnType { panic(pgerror.AssertionFailedf("unknown window return type. e:\n%s", e)) } diff --git a/pkg/sql/opt/memo/typing_test.go b/pkg/sql/opt/memo/typing_test.go index b36d4a46da92..b8ad88e894c9 100644 --- a/pkg/sql/opt/memo/typing_test.go +++ b/pkg/sql/opt/memo/typing_test.go @@ -205,7 +205,7 @@ func TestTypingAggregateAssumptions(t *testing.T) { } // Check for fixed return types. - retType := overload.ReturnType(nil) + retType := overload.ReturnType(nil /* ctx */, nil /* args */) if retType == tree.UnknownReturnType { t.Errorf("return type is not fixed for %s: %+v", name, overload.Types.Types()) } diff --git a/pkg/sql/opt/norm/custom_funcs.go b/pkg/sql/opt/norm/custom_funcs.go index 12c65bd4174d..cdc7bdd75239 100644 --- a/pkg/sql/opt/norm/custom_funcs.go +++ b/pkg/sql/opt/norm/custom_funcs.go @@ -1837,4 +1837,5 @@ var FoldFunctionWhitelist = map[string]struct{}{ "jsonb_strip_nulls": {}, "json_array_length": {}, "jsonb_array_length": {}, + "locality": {}, } diff --git a/pkg/sql/opt/optbuilder/testdata/scalar b/pkg/sql/opt/optbuilder/testdata/scalar index 5b5347f25950..66e605ad213a 100644 --- a/pkg/sql/opt/optbuilder/testdata/scalar +++ b/pkg/sql/opt/optbuilder/testdata/scalar @@ -1175,3 +1175,29 @@ if-err [type=decimal] │ └── variable: @1 [type=decimal] └── err-code └── const: '10000' [type=string] + +build-scalar +locality() +---- +function: locality [type=tuple] + +build locality=(region=east,dc=east1-b) +SELECT (locality()).dc +---- +project + ├── columns: dc:1(string) + ├── values + │ └── tuple [type=tuple] + └── projections + └── column-access: 1 [type=string] + └── function: locality [type=tuple{string AS region, string AS dc}] + +build +SELECT (locality()).unk +---- +error (42804): could not identify column "unk" in tuple + +build locality=(region=east,dc=east1-b) +SELECT (locality()).unk +---- +error (42804): could not identify column "unk" in tuple{string AS region, string AS dc} diff --git a/pkg/sql/opt/testutils/opttester/opt_tester.go b/pkg/sql/opt/testutils/opttester/opt_tester.go index 4586186d3d6a..b140047f1d4d 100644 --- a/pkg/sql/opt/testutils/opttester/opt_tester.go +++ b/pkg/sql/opt/testutils/opttester/opt_tester.go @@ -257,6 +257,7 @@ func (ot *OptTester) RunCommand(tb testing.TB, d *datadriven.TestData) string { ot.Flags.Verbose = testing.Verbose() ot.evalCtx.TestingKnobs.OptimizerCostPerturbation = ot.Flags.PerturbCost ot.evalCtx.Locality = ot.Flags.Locality + ot.semaCtx.Locality = ot.Flags.Locality switch d.Cmd { case "exec-ddl": diff --git a/pkg/sql/pg_catalog.go b/pkg/sql/pg_catalog.go index 4653abfc84fc..e083effb6713 100644 --- a/pkg/sql/pg_catalog.go +++ b/pkg/sql/pg_catalog.go @@ -1510,7 +1510,7 @@ CREATE TABLE pg_catalog.pg_operator ( panic(fmt.Sprintf("Unexpected operator %s with %d params", opName, params.Length())) } - returnType := tree.NewDOid(tree.DInt(returnTyper(nil).Oid())) + returnType := tree.NewDOid(tree.DInt(returnTyper(nil /* ctx */, nil /* args */).Oid())) err := addRow( h.OperatorOid(opName, leftType, rightType, returnType), // oid diff --git a/pkg/sql/planner.go b/pkg/sql/planner.go index 3ad6fb35b7f0..eae705e76226 100644 --- a/pkg/sql/planner.go +++ b/pkg/sql/planner.go @@ -252,6 +252,7 @@ func newInternalPlanner( p.semaCtx = tree.MakeSemaContext() p.semaCtx.Location = &sd.DataConversion.Location p.semaCtx.SearchPath = sd.SearchPath + p.semaCtx.Locality = execCfg.Locality plannerMon := mon.MakeUnlimitedMonitor(ctx, fmt.Sprintf("internal-planner.%s.%s", user, opName), diff --git a/pkg/sql/sem/builtins/aggregate_builtins.go b/pkg/sql/sem/builtins/aggregate_builtins.go index e92b190dcd69..d48d7785cf12 100644 --- a/pkg/sql/sem/builtins/aggregate_builtins.go +++ b/pkg/sql/sem/builtins/aggregate_builtins.go @@ -107,7 +107,7 @@ var aggregates = map[string]builtinDefinition{ arrayBuiltin(func(t *types.T) tree.Overload { return makeAggOverloadWithReturnType( []*types.T{t}, - func(args []tree.TypedExpr) *types.T { + func(ctx *tree.SemaContext, args []tree.TypedExpr) *types.T { if len(args) == 0 { return types.MakeArray(t) } diff --git a/pkg/sql/sem/builtins/builtins.go b/pkg/sql/sem/builtins/builtins.go index 7f71a818b805..f05e556fdd92 100644 --- a/pkg/sql/sem/builtins/builtins.go +++ b/pkg/sql/sem/builtins/builtins.go @@ -2756,6 +2756,31 @@ may increase either contention or retry errors, or both.`, }, ), + "locality": makeBuiltin( + tree.FunctionProperties{Category: categorySystemInfo}, + tree.Overload{ + Types: tree.ArgTypes{}, + ReturnType: func(ctx *tree.SemaContext, _ []tree.TypedExpr) *types.T { + if ctx == nil { + // Use simplified tuple type for signature (e.g. for docs). + return types.AnyTuple + } + return localityReturnType(&ctx.Locality) + }, + Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) { + datums := make(tree.Datums, len(ctx.Locality.Tiers)) + for i := range ctx.Locality.Tiers { + datums[i] = tree.NewDString(ctx.Locality.Tiers[i].Value) + } + typ := localityReturnType(&ctx.Locality) + return tree.NewDTuple(typ, datums...), nil + }, + Info: "Returns the hierarchical location of the current node as a tuple " + + "of labeled values, ordered from most inclusive to least inclusive. \n\n" + + "For example: `region=east,datacenter=us-east-1`.", + }, + ), + "crdb_internal.node_executable_version": makeBuiltin( tree.FunctionProperties{Category: categorySystemInfo}, tree.Overload{ @@ -4628,3 +4653,27 @@ func recentTimestamp(ctx *tree.EvalContext) (time.Time, error) { } return ctx.StmtTimestamp.Add(offset), nil } + +// localityReturnType returns the type that the locality() function will return. +// This is a tuple type with labeled string fields, one for each locality tier: +// +// tuple{string AS , string AS , ...} +// +// For example, consider this locality: +// +// region=east,datacenter=us-east-1 +// +// The tuple type will be: +// +// tuple{string AS region, string AS datacenter} +// +func localityReturnType(locality *roachpb.Locality) *types.T { + contents := make([]types.T, len(locality.Tiers)) + labels := make([]string, len(locality.Tiers)) + for i := range locality.Tiers { + tier := &locality.Tiers[i] + contents[i] = *types.String + labels[i] = tier.Key + } + return types.MakeLabeledTuple(contents, labels) +} diff --git a/pkg/sql/sem/builtins/generator_builtins.go b/pkg/sql/sem/builtins/generator_builtins.go index 79f0297908f1..0947cb9081f8 100644 --- a/pkg/sql/sem/builtins/generator_builtins.go +++ b/pkg/sql/sem/builtins/generator_builtins.go @@ -109,7 +109,7 @@ var generators = map[string]builtinDefinition{ // See https://www.postgresql.org/docs/current/static/functions-array.html makeGeneratorOverloadWithReturnType( tree.ArgTypes{{"input", types.AnyArray}}, - func(args []tree.TypedExpr) *types.T { + func(ctx *tree.SemaContext, args []tree.TypedExpr) *types.T { if len(args) == 0 || args[0].ResolvedType().Family() == types.UnknownFamily { return tree.UnknownReturnType } @@ -123,7 +123,7 @@ var generators = map[string]builtinDefinition{ "information_schema._pg_expandarray": makeBuiltin(genProps(expandArrayValueGeneratorLabels), makeGeneratorOverloadWithReturnType( tree.ArgTypes{{"input", types.AnyArray}}, - func(args []tree.TypedExpr) *types.T { + func(ctx *tree.SemaContext, args []tree.TypedExpr) *types.T { if len(args) == 0 || args[0].ResolvedType().Family() == types.UnknownFamily { return tree.UnknownReturnType } diff --git a/pkg/sql/sem/tree/overload.go b/pkg/sql/sem/tree/overload.go index b9faeebc17ce..0fc431d119cf 100644 --- a/pkg/sql/sem/tree/overload.go +++ b/pkg/sql/sem/tree/overload.go @@ -327,17 +327,17 @@ var UnknownReturnType *types.T // ReturnTyper defines the type-level function in which a builtin function's return type // is determined. ReturnTypers should make sure to return unknownReturnType when necessary. -type ReturnTyper func(args []TypedExpr) *types.T +type ReturnTyper func(ctx *SemaContext, args []TypedExpr) *types.T // FixedReturnType functions simply return a fixed type, independent of argument types. func FixedReturnType(typ *types.T) ReturnTyper { - return func(args []TypedExpr) *types.T { return typ } + return func(_ *SemaContext, args []TypedExpr) *types.T { return typ } } // IdentityReturnType creates a returnType that is a projection of the idx'th // argument type. func IdentityReturnType(idx int) ReturnTyper { - return func(args []TypedExpr) *types.T { + return func(_ *SemaContext, args []TypedExpr) *types.T { if len(args) == 0 { return UnknownReturnType } @@ -351,7 +351,7 @@ func IdentityReturnType(idx int) ReturnTyper { // with HomogeneousType functions, in which all arguments have been checked to // have the same type (or be null). func FirstNonNullReturnType() ReturnTyper { - return func(args []TypedExpr) *types.T { + return func(_ *SemaContext, args []TypedExpr) *types.T { if len(args) == 0 { return UnknownReturnType } @@ -365,7 +365,7 @@ func FirstNonNullReturnType() ReturnTyper { } func returnTypeToFixedType(s ReturnTyper) *types.T { - if t := s(nil); t != UnknownReturnType { + if t := s(nil, nil); t != UnknownReturnType { return t } return types.Any @@ -498,7 +498,7 @@ func typeCheckOverloadedExprs( // fixed return types. This could be improved, but is not currently // critical because we have no cases of functions with multiple // overloads that do not all expose FixedReturnTypes. - if t := o.returnType()(nil); t != UnknownReturnType { + if t := o.returnType()(ctx, nil); t != UnknownReturnType { return t.Equivalent(desired) } return true diff --git a/pkg/sql/sem/tree/type_check.go b/pkg/sql/sem/tree/type_check.go index b18ab6977931..21391aee20fb 100644 --- a/pkg/sql/sem/tree/type_check.go +++ b/pkg/sql/sem/tree/type_check.go @@ -19,6 +19,7 @@ import ( "strings" "time" + "github.com/cockroachdb/cockroach/pkg/roachpb" "github.com/cockroachdb/cockroach/pkg/server/telemetry" "github.com/cockroachdb/cockroach/pkg/sql/lex" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" @@ -58,6 +59,14 @@ type SemaContext struct { // globally for the entire txn and this field would not be needed. AsOfTimestamp *hlc.Timestamp + // Locality contains the location of the current node as a set of user-defined + // key/value pairs, ordered from most inclusive to least inclusive. If there + // are no tiers, then the node's location is not known. Example: + // + // [region=us,dc=east] + // + Locality roachpb.Locality + Properties SemaProperties } @@ -357,7 +366,7 @@ func (expr *BinaryExpr) TypeCheck(ctx *SemaContext, desired *types.T) (TypedExpr expr.Left, expr.Right = leftTyped, rightTyped expr.fn = binOp - expr.typ = binOp.returnType()(typedSubExprs) + expr.typ = binOp.returnType()(ctx, typedSubExprs) return expr, nil } @@ -559,7 +568,7 @@ func (expr *TupleStar) TypeCheck(ctx *SemaContext, desired *types.T) (TypedExpr, // Alghough we're going to elide the tuple star, we need to ensure // the expression is indeed a labeled tuple first. - if resolvedType.Family() != types.TupleFamily || len(resolvedType.TupleLabels()) == 0 { + if resolvedType.Family() != types.TupleFamily || resolvedType.TupleLabels() == nil { return nil, NewTypeIsNotCompositeError(resolvedType) } @@ -585,7 +594,7 @@ func (expr *ColumnAccessExpr) TypeCheck(ctx *SemaContext, desired *types.T) (Typ expr.Expr = subExpr resolvedType := subExpr.ResolvedType() - if resolvedType.Family() != types.TupleFamily || len(resolvedType.TupleLabels()) == 0 { + if resolvedType.Family() != types.TupleFamily || resolvedType.TupleLabels() == nil { return nil, NewTypeIsNotCompositeError(resolvedType) } @@ -922,7 +931,7 @@ func (expr *FuncExpr) TypeCheck(ctx *SemaContext, desired *types.T) (TypedExpr, } expr.fn = overloadImpl expr.fnProps = &def.FunctionProperties - expr.typ = overloadImpl.returnType()(typedSubExprs) + expr.typ = overloadImpl.returnType()(ctx, typedSubExprs) if expr.typ == UnknownReturnType { typeNames := make([]string, 0, len(expr.Exprs)) for _, expr := range typedSubExprs { @@ -1213,7 +1222,7 @@ func (expr *UnaryExpr) TypeCheck(ctx *SemaContext, desired *types.T) (TypedExpr, expr.Expr = exprTyped expr.fn = unaryOp - expr.typ = unaryOp.returnType()(typedSubExprs) + expr.typ = unaryOp.returnType()(ctx, typedSubExprs) return expr, nil }