diff --git a/pkg/sql/logictest/testdata/logic_test/annotate b/pkg/sql/logictest/testdata/logic_test/annotate new file mode 100644 index 000000000000..cca8cf93cd73 --- /dev/null +++ b/pkg/sql/logictest/testdata/logic_test/annotate @@ -0,0 +1,58 @@ + +query T +SELECT pg_typeof(NULL:::float); +---- +unknown + +query T +SELECT pg_typeof(NULL:::int:::float); +---- +unknown + +query T +SELECT pg_typeof(min(NULL:::FLOAT)) +---- +unknown + +query T +SELECT pg_typeof(min(CASE WHEN true THEN NULL:::FLOAT END)) +---- +unknown + +query T +SELECT pg_typeof(min((CASE WHEN true THEN NULL END):::FLOAT)) +---- +unknown + +query B +SELECT min(NULL:::FLOAT) IS NOT NAN; +---- +NULL + +query B +SELECT min((CASE WHEN true THEN NULL END):::FLOAT) IS NOT NAN; +---- +NULL + +query T +SELECT CASE WHEN true THEN ('a', 2) ELSE NULL:::RECORD END +---- +(a,2) + + +query T +SELECT CASE WHEN true THEN NULL:::RECORD ELSE ('a', 2) END +---- +NULL + + +query T +SELECT (SELECT NULL):::FLOAT +---- +NULL + + +query T +SELECT (SELECT NULL):::RECORD +---- +NULL diff --git a/pkg/sql/logictest/tests/3node-tenant/generated_test.go b/pkg/sql/logictest/tests/3node-tenant/generated_test.go index 312d7e481193..33118074756d 100644 --- a/pkg/sql/logictest/tests/3node-tenant/generated_test.go +++ b/pkg/sql/logictest/tests/3node-tenant/generated_test.go @@ -261,6 +261,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/fakedist-disk/generated_test.go b/pkg/sql/logictest/tests/fakedist-disk/generated_test.go index c3fb386ab248..75d2fbfa25a9 100644 --- a/pkg/sql/logictest/tests/fakedist-disk/generated_test.go +++ b/pkg/sql/logictest/tests/fakedist-disk/generated_test.go @@ -253,6 +253,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/fakedist-vec-off/generated_test.go b/pkg/sql/logictest/tests/fakedist-vec-off/generated_test.go index daee884ee323..925915d89c8d 100644 --- a/pkg/sql/logictest/tests/fakedist-vec-off/generated_test.go +++ b/pkg/sql/logictest/tests/fakedist-vec-off/generated_test.go @@ -253,6 +253,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/fakedist/generated_test.go b/pkg/sql/logictest/tests/fakedist/generated_test.go index c1a243409e69..e7d7ba9308b1 100644 --- a/pkg/sql/logictest/tests/fakedist/generated_test.go +++ b/pkg/sql/logictest/tests/fakedist/generated_test.go @@ -253,6 +253,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-legacy-schema-changer/generated_test.go b/pkg/sql/logictest/tests/local-legacy-schema-changer/generated_test.go index 1e0fccbe60b7..026083999e02 100644 --- a/pkg/sql/logictest/tests/local-legacy-schema-changer/generated_test.go +++ b/pkg/sql/logictest/tests/local-legacy-schema-changer/generated_test.go @@ -253,6 +253,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-mixed-25.4/generated_test.go b/pkg/sql/logictest/tests/local-mixed-25.4/generated_test.go index 6ce72016c694..120d8b4e9c40 100644 --- a/pkg/sql/logictest/tests/local-mixed-25.4/generated_test.go +++ b/pkg/sql/logictest/tests/local-mixed-25.4/generated_test.go @@ -253,6 +253,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-mixed-26.1/generated_test.go b/pkg/sql/logictest/tests/local-mixed-26.1/generated_test.go index 7ab0b5f4cd6a..19603a8b37d4 100644 --- a/pkg/sql/logictest/tests/local-mixed-26.1/generated_test.go +++ b/pkg/sql/logictest/tests/local-mixed-26.1/generated_test.go @@ -253,6 +253,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-mixed-26.2/generated_test.go b/pkg/sql/logictest/tests/local-mixed-26.2/generated_test.go index c07bb666410f..124a0dedda75 100644 --- a/pkg/sql/logictest/tests/local-mixed-26.2/generated_test.go +++ b/pkg/sql/logictest/tests/local-mixed-26.2/generated_test.go @@ -253,6 +253,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-prepared/generated_test.go b/pkg/sql/logictest/tests/local-prepared/generated_test.go index 9c535af2b91f..f74523df8a16 100644 --- a/pkg/sql/logictest/tests/local-prepared/generated_test.go +++ b/pkg/sql/logictest/tests/local-prepared/generated_test.go @@ -246,6 +246,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-read-committed/generated_test.go b/pkg/sql/logictest/tests/local-read-committed/generated_test.go index a05966b0e9df..5eb0d94c0350 100644 --- a/pkg/sql/logictest/tests/local-read-committed/generated_test.go +++ b/pkg/sql/logictest/tests/local-read-committed/generated_test.go @@ -253,6 +253,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-repeatable-read/generated_test.go b/pkg/sql/logictest/tests/local-repeatable-read/generated_test.go index 8cfa10cee7ea..00fc3e3d41ac 100644 --- a/pkg/sql/logictest/tests/local-repeatable-read/generated_test.go +++ b/pkg/sql/logictest/tests/local-repeatable-read/generated_test.go @@ -253,6 +253,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local-vec-off/generated_test.go b/pkg/sql/logictest/tests/local-vec-off/generated_test.go index 55dea6721ca5..5091d9b6109f 100644 --- a/pkg/sql/logictest/tests/local-vec-off/generated_test.go +++ b/pkg/sql/logictest/tests/local-vec-off/generated_test.go @@ -253,6 +253,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/local/generated_test.go b/pkg/sql/logictest/tests/local/generated_test.go index a8a9e9787bc9..34991b8530b9 100644 --- a/pkg/sql/logictest/tests/local/generated_test.go +++ b/pkg/sql/logictest/tests/local/generated_test.go @@ -274,6 +274,13 @@ func TestLogic_and_or( runLogicTest(t, "and_or") } +func TestLogic_annotate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "annotate") +} + func TestLogic_apply_join( t *testing.T, ) { diff --git a/pkg/sql/opt/optbuilder/scalar.go b/pkg/sql/opt/optbuilder/scalar.go index 9a4068580129..8975c4cbc9e5 100644 --- a/pkg/sql/opt/optbuilder/scalar.go +++ b/pkg/sql/opt/optbuilder/scalar.go @@ -281,6 +281,17 @@ func (b *Builder) buildScalar( arg := b.buildScalar(texpr, inScope, nil, nil, colRefs) out = b.factory.ConstructCast(arg, t.ResolvedType()) + case *tree.AnnotateTypeExpr: + texpr := t.TypedInnerExpr() + if texpr.ResolvedType() == types.Unknown { + // If the inner expr is still unknown cast to its type to preserve the the annotation + arg := b.buildScalar(texpr, inScope, nil, nil, colRefs) + out = b.factory.ConstructCast(arg, t.ResolvedType()) + } else { + // Treat AnnotateTypeExpr as if it wasn't present. + return b.buildScalar(texpr, inScope, outScope, outCol, colRefs) + } + case *tree.CoalesceExpr: args := make(memo.ScalarListExpr, len(t.Exprs)) typ := t.ResolvedType() diff --git a/pkg/sql/opt/optbuilder/testdata/aggregate b/pkg/sql/opt/optbuilder/testdata/aggregate index 7283c406d6f6..b3ad76b85ffa 100644 --- a/pkg/sql/opt/optbuilder/testdata/aggregate +++ b/pkg/sql/opt/optbuilder/testdata/aggregate @@ -4859,3 +4859,73 @@ scalar-group-by └── aggregations └── const-agg [as=string_agg:8] └── string_agg:8 + +build format=(show-types,show-scalars) +SELECT min(NULL:::FLOAT); +---- +scalar-group-by + ├── columns: min:2(float) + ├── project + │ ├── columns: column1:1(float) + │ ├── values + │ │ └── tuple [type=tuple] + │ └── projections + │ └── cast: FLOAT8 [as=column1:1, type=float] + │ └── null [type=unknown] + └── aggregations + └── min [as=min:2, type=float] + └── variable: column1:1 [type=float] + + +build format=(show-types,show-scalars) +SELECT min(1:::FLOAT); +---- +scalar-group-by + ├── columns: min:2(float) + ├── project + │ ├── columns: column1:1(float!null) + │ ├── values + │ │ └── tuple [type=tuple] + │ └── projections + │ └── const: 1.0 [as=column1:1, type=float] + └── aggregations + └── min [as=min:2, type=float] + └── variable: column1:1 [type=float] + +build +SELECT min(CASE WHEN true THEN NULL:::FLOAT END) IS NOT NAN; +---- +project + ├── columns: "?column?":3 + ├── scalar-group-by + │ ├── columns: min:2 + │ ├── project + │ │ ├── columns: column1:1 + │ │ ├── values + │ │ │ └── () + │ │ └── projections + │ │ └── CASE WHEN true THEN NULL::FLOAT8 ELSE CAST(NULL AS FLOAT8) END [as=column1:1] + │ └── aggregations + │ └── min [as=min:2] + │ └── column1:1 + └── projections + └── min:2 != NaN [as="?column?":3] + +build +SELECT min((CASE WHEN true THEN NULL END):::FLOAT) IS NOT NAN; +---- +project + ├── columns: "?column?":3 + ├── scalar-group-by + │ ├── columns: min:2 + │ ├── project + │ │ ├── columns: column1:1 + │ │ ├── values + │ │ │ └── () + │ │ └── projections + │ │ └── CASE WHEN true THEN NULL::FLOAT8 ELSE CAST(NULL AS FLOAT8) END [as=column1:1] + │ └── aggregations + │ └── min [as=min:2] + │ └── column1:1 + └── projections + └── min:2 != NaN [as="?column?":3] diff --git a/pkg/sql/opt/optbuilder/testdata/explain b/pkg/sql/opt/optbuilder/testdata/explain index 3d342dea634c..424658ff0e59 100644 --- a/pkg/sql/opt/optbuilder/testdata/explain +++ b/pkg/sql/opt/optbuilder/testdata/explain @@ -66,3 +66,67 @@ explain │ └── (3, 4) └── filters └── x:1 = column1:5 + + +build +EXPLAIN (VERBOSE) SELECT CASE WHEN true THEN ('a', 2) ELSE NULL:::RECORD END +---- +explain + ├── columns: info:2 + ├── mode: verbose + └── project + ├── columns: case:1 + ├── values + │ └── () + └── projections + └── CASE WHEN true THEN ('a', 2) ELSE NULL::RECORD END [as=case:1] + + +build +EXPLAIN (VERBOSE) SELECT NULL:::FLOAT; +---- +explain + ├── columns: info:2 + ├── mode: verbose + └── project + ├── columns: float8:1 + ├── values + │ └── () + └── projections + └── NULL::FLOAT8 [as=float8:1] + + +build +EXPLAIN (VERBOSE) SELECT NULL:::int:::float; +---- +explain + ├── columns: info:2 + ├── mode: verbose + └── project + ├── columns: float8:1 + ├── values + │ └── () + └── projections + └── NULL::FLOAT8 [as=float8:1] + +build +EXPLAIN (VERBOSE) SELECT (SELECT NULL):::FLOAT +---- +explain + ├── columns: info:3 + ├── mode: verbose + └── project + ├── columns: float8:2 + ├── values + │ └── () + └── projections + └── cast: FLOAT8 [as=float8:2] + └── subquery + └── max1-row + ├── columns: "?column?":1 + └── project + ├── columns: "?column?":1 + ├── values + │ └── () + └── projections + └── NULL [as="?column?":1] diff --git a/pkg/sql/sem/eval/expr.go b/pkg/sql/sem/eval/expr.go index 9b6442c29ae3..82380e153711 100644 --- a/pkg/sql/sem/eval/expr.go +++ b/pkg/sql/sem/eval/expr.go @@ -125,6 +125,12 @@ func (e *evaluator) EvalBinaryExpr(ctx context.Context, expr *tree.BinaryExpr) ( return res, err } +func (e *evaluator) EvalAnnotateTypeExpr( + ctx context.Context, expr *tree.AnnotateTypeExpr, +) (tree.Datum, error) { + return expr.TypedInnerExpr().Eval(ctx, e) +} + func (e *evaluator) EvalCaseExpr(ctx context.Context, expr *tree.CaseExpr) (tree.Datum, error) { if expr.Expr != nil { // CASE WHEN THEN ... diff --git a/pkg/sql/sem/tree/eval_expr_generated.go b/pkg/sql/sem/tree/eval_expr_generated.go index 4e039a183e0e..9bd86073d5ea 100644 --- a/pkg/sql/sem/tree/eval_expr_generated.go +++ b/pkg/sql/sem/tree/eval_expr_generated.go @@ -19,6 +19,7 @@ import "context" type ExprEvaluator interface { EvalAllColumnsSelector(context.Context, *AllColumnsSelector) (Datum, error) EvalAndExpr(context.Context, *AndExpr) (Datum, error) + EvalAnnotateTypeExpr(context.Context, *AnnotateTypeExpr) (Datum, error) EvalArray(context.Context, *Array) (Datum, error) EvalArrayFlatten(context.Context, *ArrayFlatten) (Datum, error) EvalBinaryExpr(context.Context, *BinaryExpr) (Datum, error) @@ -66,6 +67,11 @@ func (node *AndExpr) Eval(ctx context.Context, v ExprEvaluator) (Datum, error) { return v.EvalAndExpr(ctx, node) } +// Eval is part of the TypedExpr interface. +func (node *AnnotateTypeExpr) Eval(ctx context.Context, v ExprEvaluator) (Datum, error) { + return v.EvalAnnotateTypeExpr(ctx, node) +} + // Eval is part of the TypedExpr interface. func (node *Array) Eval(ctx context.Context, v ExprEvaluator) (Datum, error) { return v.EvalArray(ctx, node) diff --git a/pkg/sql/sem/tree/expr.go b/pkg/sql/sem/tree/expr.go index 340e8ae3cac7..b471726e40c6 100644 --- a/pkg/sql/sem/tree/expr.go +++ b/pkg/sql/sem/tree/expr.go @@ -1632,6 +1632,8 @@ type AnnotateTypeExpr struct { Type ResolvableTypeReference SyntaxMode annotateSyntaxMode + + typeAnnotation } // Format implements the NodeFormatter interface. diff --git a/pkg/sql/sem/tree/type_check.go b/pkg/sql/sem/tree/type_check.go index 7814459d5b5d..3d10dfae3fbf 100644 --- a/pkg/sql/sem/tree/type_check.go +++ b/pkg/sql/sem/tree/type_check.go @@ -835,7 +835,16 @@ func (expr *AnnotateTypeExpr) TypeCheck( if err != nil { return nil, err } - return subExpr, nil + + // Return subExpr if the AnnotateTypeExpr is redundant. + if subExpr.ResolvedType() != types.Unknown || annotateType.IsAmbiguous() || cast.ValidCast(annotateType, desired, cast.ContextImplicit) { + return subExpr, nil + } + + expr.Expr = subExpr + expr.typ = annotateType + + return expr, nil } // TypeCheck implements the Expr interface.