From c402b02b9fb53aee2a26876de90a8f95f9a9be92 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 12 Apr 2021 14:37:22 -0400 Subject: [PATCH] Fix old bug with coercing the result of a COLLATE expression. There are hacks in parse_coerce.c to push down a requested coercion to below any CollateExpr that may appear. However, we did that even if the requested data type is non-collatable, leading to an invalid expression tree in which CollateExpr is applied to a non-collatable type. The fix is just to drop the CollateExpr altogether, reasoning that it's useless. This bug is ten years old, dating to the original addition of COLLATE support. The lack of field complaints suggests that there aren't a lot of user-visible consequences. We noticed the problem because it would trigger an assertion in DefineVirtualRelation if the invalid structure appears as an output column of a view; however, in a non-assert build, you don't see a crash just a (subtly incorrect) complaint about applying collation to a non-collatable type. I found that by putting the incorrect structure further down in a view, I could make a view definition that would fail dump/reload, per the added regression test case. But CollateExpr doesn't do anything at run-time, so this likely doesn't lead to any really exciting consequences. Per report from Yulin Pei. Back-patch to all supported branches. Discussion: https://postgr.es/m/HK0PR01MB22744393C474D503E16C8509F4709@HK0PR01MB2274.apcprd01.prod.exchangelabs.com --- src/backend/parser/parse_coerce.c | 29 +++++++++++++++++---------- src/test/regress/expected/collate.out | 16 ++++++++++++++- src/test/regress/sql/collate.sql | 6 ++++++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c index d5310f27db..aa4a21126d 100644 --- a/src/backend/parser/parse_coerce.c +++ b/src/backend/parser/parse_coerce.c @@ -95,6 +95,7 @@ coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype, * *must* know that to avoid possibly calling hide_coercion_node on * something that wasn't generated by coerce_type. Note that if there are * multiple stacked CollateExprs, we just discard all but the topmost. + * Also, if the target type isn't collatable, we discard the CollateExpr. */ origexpr = expr; while (expr && IsA(expr, CollateExpr)) @@ -114,7 +115,7 @@ coerce_to_target_type(ParseState *pstate, Node *expr, Oid exprtype, ccontext, cformat, location, (result != expr && !IsA(result, Const))); - if (expr != origexpr) + if (expr != origexpr && type_is_collatable(targettype)) { /* Reinstall top CollateExpr */ CollateExpr *coll = (CollateExpr *) origexpr; @@ -388,20 +389,26 @@ coerce_type(ParseState *pstate, Node *node, { /* * If we have a COLLATE clause, we have to push the coercion - * underneath the COLLATE. This is really ugly, but there is little - * choice because the above hacks on Consts and Params wouldn't happen + * underneath the COLLATE; or discard the COLLATE if the target type + * isn't collatable. This is really ugly, but there is little choice + * because the above hacks on Consts and Params wouldn't happen * otherwise. This kluge has consequences in coerce_to_target_type. */ CollateExpr *coll = (CollateExpr *) node; - CollateExpr *newcoll = makeNode(CollateExpr); - newcoll->arg = (Expr *) - coerce_type(pstate, (Node *) coll->arg, - inputTypeId, targetTypeId, targetTypeMod, - ccontext, cformat, location); - newcoll->collOid = coll->collOid; - newcoll->location = coll->location; - return (Node *) newcoll; + result = coerce_type(pstate, (Node *) coll->arg, + inputTypeId, targetTypeId, targetTypeMod, + ccontext, cformat, location); + if (type_is_collatable(targetTypeId)) + { + CollateExpr *newcoll = makeNode(CollateExpr); + + newcoll->arg = (Expr *) result; + newcoll->collOid = coll->collOid; + newcoll->location = coll->location; + result = (Node *) newcoll; + } + return result; } pathtype = find_coercion_pathway(targetTypeId, inputTypeId, ccontext, &funcId); diff --git a/src/test/regress/expected/collate.out b/src/test/regress/expected/collate.out index c42ab8f703..0b60b55514 100644 --- a/src/test/regress/expected/collate.out +++ b/src/test/regress/expected/collate.out @@ -688,13 +688,26 @@ SELECT collation for ((SELECT b FROM collate_test1 LIMIT 1)); "C" (1 row) +-- old bug with not dropping COLLATE when coercing to non-collatable type +CREATE VIEW collate_on_int AS +SELECT c1+1 AS c1p FROM + (SELECT ('4' COLLATE "C")::INT AS c1) ss; +\d+ collate_on_int + View "collate_tests.collate_on_int" + Column | Type | Collation | Nullable | Default | Storage | Description +--------+---------+-----------+----------+---------+---------+------------- + c1p | integer | | | | plain | +View definition: + SELECT ss.c1 + 1 AS c1p + FROM ( SELECT 4 AS c1) ss; + -- -- Clean up. Many of these table names will be re-used if the user is -- trying to run any platform-specific collation tests later, so we -- must get rid of them. -- DROP SCHEMA collate_tests CASCADE; -NOTICE: drop cascades to 18 other objects +NOTICE: drop cascades to 19 other objects DETAIL: drop cascades to table collate_test1 drop cascades to table collate_test_like drop cascades to table collate_test2 @@ -713,3 +726,4 @@ drop cascades to table collate_test21 drop cascades to table collate_test22 drop cascades to collation mycoll2 drop cascades to table collate_test23 +drop cascades to view collate_on_int diff --git a/src/test/regress/sql/collate.sql b/src/test/regress/sql/collate.sql index 82f9c855b8..30f811ba89 100644 --- a/src/test/regress/sql/collate.sql +++ b/src/test/regress/sql/collate.sql @@ -266,6 +266,12 @@ SELECT collation for ('foo'::text); SELECT collation for ((SELECT a FROM collate_test1 LIMIT 1)); -- non-collatable type - error SELECT collation for ((SELECT b FROM collate_test1 LIMIT 1)); +-- old bug with not dropping COLLATE when coercing to non-collatable type +CREATE VIEW collate_on_int AS +SELECT c1+1 AS c1p FROM + (SELECT ('4' COLLATE "C")::INT AS c1) ss; +\d+ collate_on_int + -- -- Clean up. Many of these table names will be re-used if the user is