From a0ba34c875f81775a6a4bf902b565bfba944adf9 Mon Sep 17 00:00:00 2001 From: Corey Huinker Date: Thu, 5 Nov 2026 21:47:13 -0500 Subject: [PATCH v35 5/5] WIP - Short circuit default of CAST DEFAULT ON CONVERSION ERROR. Prevent the default clause of a CAST(... DEFAULT ... ON CONVERSION ERROR) statement from being evaluated if the CAST conversion succeeds. Generated with an LLM. --- src/backend/optimizer/util/clauses.c | 104 ++++++++++++++++++--------- src/test/regress/expected/cast.out | 45 ++++++++++-- src/test/regress/sql/cast.sql | 13 ++++ 3 files changed, 121 insertions(+), 41 deletions(-) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 532028f794c..88838ff937c 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -72,6 +72,7 @@ typedef struct Node *case_val; bool estimate; ErrorSaveContext *escontext; + bool no_eval; /* simplify, but never evaluate */ } eval_const_expressions_context; typedef struct @@ -2692,6 +2693,7 @@ eval_const_expressions(PlannerInfo *root, Node *node) context.case_val = NULL; /* no CASE being examined */ context.estimate = false; /* safe transformations only */ context.escontext = NULL; /* for error-safe expression evaluation */ + context.no_eval = false; /* evaluate constant subexpressions */ return eval_const_expressions_mutator(node, &context); } @@ -2835,6 +2837,7 @@ estimate_expression_value(PlannerInfo *root, Node *node) context.case_val = NULL; /* no CASE being examined */ context.estimate = true; /* unsafe transformations OK */ context.escontext = NULL; /* for error-safe expression evaluation */ + context.no_eval = false; /* evaluate constant subexpressions */ return eval_const_expressions_mutator(node, &context); } @@ -2859,13 +2862,17 @@ estimate_expression_value(PlannerInfo *root, Node *node) #define ece_all_arguments_const(node) \ (!expression_tree_walker((Node *) (node), contain_non_const_walker, NULL)) -/* Generic macro for applying evaluate_expr */ -#define ece_evaluate_expr(node, escontext) \ - ((Node *) evaluate_expr((Expr *) (node), \ +/* + * Generic macro for applying evaluate_expr, unless the context forbids any + * evaluation, in which case the node is returned as it is. + */ +#define ece_evaluate_expr(node, context) \ + ((context)->no_eval ? (Node *) (node) : \ + (Node *) evaluate_expr((Expr *) (node), \ exprType((Node *) (node)), \ exprTypmod((Node *) (node)), \ exprCollation((Node *) (node)), \ - (Node *) escontext)) + (Node *) (context)->escontext)) /* * Recursive guts of eval_const_expressions/estimate_expression_value @@ -3336,7 +3343,7 @@ eval_const_expressions_mutator(Node *node, if (!has_nonconst_input && ece_function_is_safe(expr->opfuncid, context)) - return ece_evaluate_expr(expr, context->escontext); + return ece_evaluate_expr(expr, context); return (Node *) expr; } @@ -3356,7 +3363,7 @@ eval_const_expressions_mutator(Node *node, */ if (ece_all_arguments_const(saop) && ece_function_is_safe(saop->opfuncid, context)) - return ece_evaluate_expr(saop, context->escontext); + return ece_evaluate_expr(saop, context); return (Node *) saop; } case T_BoolExpr: @@ -3519,14 +3526,18 @@ eval_const_expressions_mutator(Node *node, Node *defexpr; ErrorSaveContext *save_escontext = context->escontext; Node *save_case_val = context->case_val; + bool save_no_eval = context->no_eval; ErrorSaveContext *escontext = makeNode(ErrorSaveContext); - if (stc->castexpr == NULL) - return eval_const_expressions_mutator((Node *) stc->defexpr, - context); - + /* + * The DEFAULT is evaluated only if the conversion fails, so + * simplify it without evaluating anything: it might raise an + * error that must not occur when the conversion succeeds. + */ + context->no_eval = true; defexpr = eval_const_expressions_mutator((Node *) stc->defexpr, context); + context->no_eval = save_no_eval; /* * arg is normally evaluated under the caller's context. If @@ -3539,11 +3550,36 @@ eval_const_expressions_mutator(Node *node, context->escontext = save_escontext; /* - * A SafeCastElement failed to convert its constant element, - * so the conversion is known to fail: the result is defexpr. + * If there is no conversion at all, or a SafeCastElement + * failed to convert its constant element, the conversion is + * known to fail and the result is defexpr. But arg must + * still be evaluated for its own errors, so we can discard it + * only if it is a constant, which cannot fail. Otherwise + * keep it, with no conversion, and let the executor evaluate + * it before returning defexpr. */ - if (escontext->error_occurred) - return defexpr; + if (stc->castexpr == NULL || escontext->error_occurred) + { + /* + * The DEFAULT is now certain to be evaluated, so it can + * be simplified in the ordinary way. + */ + if (IsA(arg, Const)) + return eval_const_expressions_mutator((Node *) stc->defexpr, + context); + + newexpr = makeNode(SafeTypeCastExpr); + newexpr->arg = (Expr *) arg; + newexpr->arg_errorsafe = stc->arg_errorsafe; + newexpr->castexpr = NULL; + newexpr->defexpr = (Expr *) defexpr; + newexpr->resulttype = stc->resulttype; + newexpr->resulttypmod = stc->resulttypmod; + newexpr->resultcollid = stc->resultcollid; + newexpr->location = stc->location; + + return (Node *) newexpr; + } /* only the conversion itself is evaluated error-safe */ context->escontext = escontext; @@ -3558,7 +3594,8 @@ eval_const_expressions_mutator(Node *node, * same way at runtime. */ if (escontext->error_occurred) - return defexpr; + return eval_const_expressions_mutator((Node *) stc->defexpr, + context); if (IsA(castexpr, Const)) return castexpr; @@ -3581,21 +3618,6 @@ eval_const_expressions_mutator(Node *node, ErrorSaveContext *save_escontext = context->escontext; Node *save_case_val = context->case_val; - /* - * Once an element has failed, the enclosing SafeTypeCastExpr - * returns its DEFAULT and discards the array, so the - * remaining elements need not be processed. - */ - if (SOFT_ERROR_OCCURRED(save_escontext)) - return node; - - /* - * An element without a conversion makes the whole cast fail - * at parse time, and the SafeTypeCastExpr case then never - * gets here. - */ - Assert(elem->castexpr != NULL); - newelem = makeNode(SafeCastElement); memcpy(newelem, elem, sizeof(SafeCastElement)); @@ -3605,6 +3627,14 @@ eval_const_expressions_mutator(Node *node, eval_const_expressions_mutator((Node *) elem->arg, context); context->escontext = save_escontext; + /* + * An element without a conversion makes the enclosing cast + * return its DEFAULT, but the element itself must still be + * evaluated for its own errors. + */ + if (elem->castexpr == NULL) + return (Node *) newelem; + /* its conversion reports into the enclosing cast's context */ context->case_val = IsA(newelem->arg, Const) ? (Node *) newelem->arg : NULL; @@ -3768,7 +3798,7 @@ eval_const_expressions_mutator(Node *node, if (ac->arg && IsA(ac->arg, Const) && ac->elemexpr && !IsA(ac->elemexpr, CoerceToDomain) && !contain_mutable_functions((Node *) ac->elemexpr)) - return ece_evaluate_expr(ac, context->escontext); + return ece_evaluate_expr(ac, context); return (Node *) ac; } @@ -3971,7 +4001,7 @@ eval_const_expressions_mutator(Node *node, node = ece_generic_processing(node); /* If all arguments are Consts, we can fold to a constant */ if (ece_all_arguments_const(node)) - return ece_evaluate_expr(node, context->escontext); + return ece_evaluate_expr(node, context); return node; } case T_CoalesceExpr: @@ -4071,7 +4101,7 @@ eval_const_expressions_mutator(Node *node, if ((context->estimate || xmlexpr_is_immutable((XmlExpr *) node)) && ece_all_arguments_const(node)) - return ece_evaluate_expr(node, context->escontext); + return ece_evaluate_expr(node, context); return node; } case T_FieldSelect: @@ -4167,7 +4197,7 @@ eval_const_expressions_mutator(Node *node, newfselect->resulttype, newfselect->resulttypmod, newfselect->resultcollid)) - return ece_evaluate_expr(newfselect, context->escontext); + return ece_evaluate_expr(newfselect, context); } return (Node *) newfselect; } @@ -4505,7 +4535,7 @@ eval_const_expressions_mutator(Node *node, newcre->arg = (Expr *) arg; if (arg != NULL && IsA(arg, Const)) - return ece_evaluate_expr((Node *) newcre, context->escontext); + return ece_evaluate_expr((Node *) newcre, context); return (Node *) newcre; } default: @@ -5618,6 +5648,10 @@ evaluate_function(Oid funcid, Oid result_type, int32 result_typmod, else return NULL; + /* If we may not evaluate anything, we can't simplify it either */ + if (context->no_eval) + return NULL; + /* * OK, looks like we can simplify this operator/function. * diff --git a/src/test/regress/expected/cast.out b/src/test/regress/expected/cast.out index c46aab71546..eed30e585d8 100644 --- a/src/test/regress/expected/cast.out +++ b/src/test/regress/expected/cast.out @@ -46,7 +46,11 @@ ERROR: invalid input syntax for type integer: "b" LINE 1: SELECT CAST('a' as int DEFAULT 'b' ON CONVERSION ERROR); ^ SELECT CAST(1 AS int DEFAULT 70000::int2::int4 ON CONVERSION ERROR); -ERROR: smallint out of range + int4 +------ + 1 +(1 row) + SELECT CAST('70000' AS int2 DEFAULT -1 ON CONVERSION ERROR); int2 ------ @@ -306,11 +310,7 @@ SELECT CAST(ARRAY[e] as int[] DEFAULT NULL ON CONVERSION ERROR) FROM tcast1; -- all of the following should be accepted. For an ARRAY[] argument, each -- element is converted to the target element type SELECT CAST(ARRAY['x', a::int] AS int[] DEFAULT NULL ON CONVERSION ERROR) FROM tcast1; - array -------- - -(1 row) - +ERROR: invalid input syntax for type integer: "a" SELECT CAST(ARRAY[1,2] || 3 AS boolean DEFAULT false ON CONVERSION ERROR); bool ------ @@ -1192,3 +1192,36 @@ DROP DOMAIN dint; DROP DOMAIN dintnn; RESET extra_float_digits; RESET lc_monetary; +-- errors in the input expression itself must not be turned into the DEFAULT, +-- even when no cast path exists or the cast is known to fail +SELECT CAST(1/0 AS date DEFAULT NULL ON CONVERSION ERROR); +ERROR: division by zero +SELECT CAST((('abc'::text)::integer) AS date DEFAULT NULL ON CONVERSION ERROR); +ERROR: invalid input syntax for type integer: "abc" +SELECT CAST((1/(g-g)) AS date DEFAULT NULL ON CONVERSION ERROR) FROM generate_series(1,1) g; +ERROR: division by zero +SELECT CAST(ARRAY['x', (1/0)::text] AS date[] DEFAULT NULL ON CONVERSION ERROR); +ERROR: division by zero +SELECT CAST(ARRAY['x', (1/(g-g))::text] AS date[] DEFAULT NULL ON CONVERSION ERROR) FROM generate_series(1,1) g; +ERROR: division by zero +-- the DEFAULT is not evaluated when the conversion succeeds +SELECT CAST('1' AS int DEFAULT 1/0 ON CONVERSION ERROR); + int4 +------ + 1 +(1 row) + +SELECT CAST(g::text AS int DEFAULT 1/0 ON CONVERSION ERROR) FROM generate_series(1,1) g; + g +--- + 1 +(1 row) + +SELECT CAST(ARRAY['1'] AS int[] DEFAULT ('{' || (1/0) || '}')::int[] ON CONVERSION ERROR); + array +------- + {1} +(1 row) + +SELECT CAST('x' AS int DEFAULT 1/0 ON CONVERSION ERROR); +ERROR: division by zero diff --git a/src/test/regress/sql/cast.sql b/src/test/regress/sql/cast.sql index 35c3bddd325..1573625336a 100644 --- a/src/test/regress/sql/cast.sql +++ b/src/test/regress/sql/cast.sql @@ -459,3 +459,16 @@ DROP DOMAIN dint; DROP DOMAIN dintnn; RESET extra_float_digits; RESET lc_monetary; + +-- errors in the input expression itself must not be turned into the DEFAULT, +-- even when no cast path exists or the cast is known to fail +SELECT CAST(1/0 AS date DEFAULT NULL ON CONVERSION ERROR); +SELECT CAST((('abc'::text)::integer) AS date DEFAULT NULL ON CONVERSION ERROR); +SELECT CAST((1/(g-g)) AS date DEFAULT NULL ON CONVERSION ERROR) FROM generate_series(1,1) g; +SELECT CAST(ARRAY['x', (1/0)::text] AS date[] DEFAULT NULL ON CONVERSION ERROR); +SELECT CAST(ARRAY['x', (1/(g-g))::text] AS date[] DEFAULT NULL ON CONVERSION ERROR) FROM generate_series(1,1) g; +-- the DEFAULT is not evaluated when the conversion succeeds +SELECT CAST('1' AS int DEFAULT 1/0 ON CONVERSION ERROR); +SELECT CAST(g::text AS int DEFAULT 1/0 ON CONVERSION ERROR) FROM generate_series(1,1) g; +SELECT CAST(ARRAY['1'] AS int[] DEFAULT ('{' || (1/0) || '}')::int[] ON CONVERSION ERROR); +SELECT CAST('x' AS int DEFAULT 1/0 ON CONVERSION ERROR); -- 2.54.0 (Apple Git-157)