Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wUUIN-001C3E-1i for pgsql-bugs@arkaria.postgresql.org; Tue, 02 Jun 2026 18:56:59 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wUUIL-00FdJa-1g for pgsql-bugs@arkaria.postgresql.org; Tue, 02 Jun 2026 18:56:57 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wUKnn-00DgLf-2F for pgsql-bugs@lists.postgresql.org; Tue, 02 Jun 2026 08:48:47 +0000 Received: from mail-ot1-x32f.google.com ([2607:f8b0:4864:20::32f]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wUKnl-00000000p8t-28Aq for pgsql-bugs@lists.postgresql.org; Tue, 02 Jun 2026 08:48:47 +0000 Received: by mail-ot1-x32f.google.com with SMTP id 46e09a7af769-7e6b5dfde3cso1034135a34.3 for ; Tue, 02 Jun 2026 01:48:45 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1780390123; cv=none; d=google.com; s=arc-20240605; b=J51R41BGwmStwSt4VtYLAqCoUnQ9K6g2iP2W04Krjw9ekj5vA3GMc4h2s3J1eg5psE 0DAQEjDmqBXBfD4aQPdyjP95PKbLiNguMfxpZ5uF3t+Zr5x2mNrX+k0a6CDvfYMKKufm Oyg056k/2Ny2WIqhW2dv4MP/nlpjZ3ZMnZxJx0ndZ4gwC09acH5HZdPyolkQlBF1x+fE aXCs6DUDBBTv6bFQCxYxePVhDHGEzXRKTv/X4R+u7kNIFDCsDFpkJbTLrrjBYeJvPbu6 Fm2JIVJDQ95S6FBFfqtOsS758cARSbSSORYM8S/aSnlDktP/nEQcpFd8ux+uex+0ychd G+yQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=to:subject:message-id:date:from:mime-version:dkim-signature; bh=n7dXYHfdI8gzWLK7CrPYBsd1N+t5JVXK5GczI7q21Zw=; fh=/gQe77b11iMZdcPj/nJr/Ghqi6rQp5FPrPbdO93bmOA=; b=auog2TlFLFW4Zk6SlPghuzE4Gp4jjDfTHHWtkLjCpUKOcZgrWdaOvvhc7ZgYVUuyKX 9EIv/+NwiZBPs2bX61daGEU/dHgP/SDmSml4Twl2vtxwq3rqNQeDsyUOHpsOehL3i55N n8Zfqtc2LLPPksTbQ1mBUVsXiVHFhOMKlaw243zvBrM6z+0u6XhFIOm1gAMBeUBw2+vM aUiCt7pEx6d2FFa3yIYUhYJ6ZCENSHw6y29gaDazKQrdgyGxVJWTCg28rQat0zJoO6PZ P6FX1NPFeYw+7uY8N9lobNi8olCP2WDmED4goUjMYMwfcXZfuk1tj3CfUe3zWMT5NmVG c1wA==; darn=lists.postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=planetscale.com; s=google; t=1780390123; x=1780994923; darn=lists.postgresql.org; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=n7dXYHfdI8gzWLK7CrPYBsd1N+t5JVXK5GczI7q21Zw=; b=SG/kU8NmhtfoPEkEExlDMNCuUXifsPchvqWFrBQapY7Zz9ZCD6jcWQEkpqoZf+yYQM XY3EB4Et8MdkZqqaGGvsTQZZ+7FGxoulG74YR9/ZZIJVghZnIyc6oCdqdkMEhKr54ly6 Nks40reRfemcwM/BZoP0R5u9AZLestOd1ja+L4E1vM/615cumvQlzx8bjSOf9cfPpBYu zng0uEZ1QwWbLSRDHs86fCMh/KICgwrcmO34tYuLF5a7ZagBdDmKsY1IIt8HcnJD+vqL s5F4rT+EB9N3R10nOQvtPK4RqCddKzNx02079/vLEpt0fNRK80qjWRSF1Egmrh5hLiOE p2UA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1780390123; x=1780994923; h=to:subject:message-id:date:from:mime-version:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=n7dXYHfdI8gzWLK7CrPYBsd1N+t5JVXK5GczI7q21Zw=; b=i9rqkTVQw7QgPchDJiDE1Ts6xfPAEXvdhAJWqCbwLcWSa/lSzgeRfs9lBiEBFuptGC 3m8fKT5PkDRjuzEokVZV1/L27+mWMwkCyNwUWlrQbwv8N/+rrenxkfjIxApTFbYTz2Yl Zit1jSr/4NMDXKvsikCgO1aCiWg2Ls9MoYHAP+861pKWGaR60FhqiU4PiYJ4kv5ObMcN kz89NOZOCSsvL2n0WCVk4GHvs+GJWExYLYZQBQqXIKaZpdffsFJZPlZ8LJdcHkr4o8jW pAVtNFsAghCXse11Ejqd4SZnpxqQd1Pt4a+IKcDvoTzmCpTl9iDpdBt2fq+O2WwtqKYB isCQ== X-Gm-Message-State: AOJu0YyDfltl/V0FrAZ+FHBqCYFJziHrRsyTWMmO/6NoScA3bxw9Kl6g bg9RMPfOSuzj83C65JCKS9wHQ7NGBYMHphE7IKhE/zTtD/7nUx/unNrhpy3tuNAkzBEn6z/rBpe h30UX7pa3BIdo3jbWskRWPz4WB3IyhUnX11DjRIKeGeXvneEGmYr20sWIXg== X-Gm-Gg: Acq92OGXmRxke2EK9wIFXJqR6y6mLvHMc/gdBzNvbRIYPZ7IWQKSgR5aktUQbSa/V6j AWB3XEMu5EdVGWSeeTiPGTmd+WgLC4b+FZKf7jKvk0LUAZBKO1Sa+DbV3k6cQZW8F3QuTmmuRqb y3Qp/5s1l105wtqrmFK9ipnkdoijI6wVURgwrFs1usoqN0YFETR+e7RjDAKYIcp+VnWtLXGGnDy XpX13RD4oQ+qS/mCDM2IljDslP2T2VIDkKV9GFzIxNyOh3+QbpEar/QExFIuaoVtLLvXx1PuVA9 ycJKgRkLayIkuiTosg== X-Received: by 2002:a05:6820:c83:b0:69d:e5f6:cef5 with SMTP id 006d021491bc7-69e104092b4mr7727566eaf.55.1780390123346; Tue, 02 Jun 2026 01:48:43 -0700 (PDT) MIME-Version: 1.0 From: Mats Rydberg Date: Tue, 2 Jun 2026 10:48:32 +0200 X-Gm-Features: AVHnY4JB9SHFFzhIbtaa43bU9m8QBQ6k5wN5CV43cgs8F1OSi6Tbgw_fFRl-5To Message-ID: Subject: GREATEST/LEAST ignores comparison operator volatility in contain_volatile_functions_walker To: pgsql-bugs@lists.postgresql.org Content-Type: multipart/alternative; boundary="000000000000dd33110653416089" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --000000000000dd33110653416089 Content-Type: text/plain; charset="UTF-8" PostgreSQL version: 18.4 (also reproduced on 18.3) Platform: aarch64-apple-darwin, Apple clang 17 GREATEST and LEAST (MinMaxExpr nodes) are missing from contain_volatile_functions_walker in src/backend/optimizer/util/clauses.c. OpExpr nodes are correctly handled -- the walker resolves the oprcode via set_opfuncid and checks func_volatile. But there is no MinMaxExpr case, so GREATEST/LEAST always appears function-free to the volatility checker regardless of the < or > operator's provolatile. The practical consequence: a GREATEST/LEAST expression whose comparison operator is STABLE or VOLATILE is incorrectly treated as IMMUTABLE. The reproducer below demonstrates this via a generated column, which requires an IMMUTABLE expression. PostgreSQL accepts the column definition when it should reject it. -- Reproducer (self-contained, tested on 18.4): DROP TABLE IF EXISTS t CASCADE; DROP TYPE IF EXISTS myval CASCADE; CREATE TYPE myval; CREATE FUNCTION myval_in(cstring) RETURNS myval LANGUAGE internal STRICT IMMUTABLE AS 'textin'; CREATE FUNCTION myval_out(myval) RETURNS cstring LANGUAGE internal STRICT IMMUTABLE AS 'textout'; CREATE TYPE myval (INPUT = myval_in, OUTPUT = myval_out, LIKE = text); -- < is STABLE: result depends on the myval.reverse session GUC CREATE FUNCTION myval_lt(a myval, b myval) RETURNS boolean LANGUAGE sql STABLE STRICT AS $$ SELECT CASE current_setting('myval.reverse', true) WHEN 'on' THEN a::text > b::text ELSE a::text < b::text END $$; CREATE OPERATOR < ( leftarg = myval, rightarg = myval, procedure = myval_lt, negator = >= ); -- btree opclass so GREATEST can resolve a sort operator for myval CREATE FUNCTION myval_cmp(a myval, b myval) RETURNS integer LANGUAGE sql IMMUTABLE STRICT AS $$ SELECT CASE WHEN a::text < b::text THEN -1 WHEN a::text > b::text THEN 1 ELSE 0 END $$; CREATE OPERATOR CLASS myval_ops DEFAULT FOR TYPE myval USING btree AS OPERATOR 1 <, FUNCTION 1 myval_cmp(myval, myval); CREATE TABLE t (x myval, y myval); -- Control: a direct call to the Stable function is correctly rejected. -- ALTER TABLE t ADD COLUMN ctrl boolean -- GENERATED ALWAYS AS (myval_lt(x, y)) STORED; -- => ERROR: generation expression is not immutable (correct) -- Bug: GREATEST uses the same Stable < operator but the missing -- MinMaxExpr case in contain_volatile_functions_walker means -- PostgreSQL treats the expression as immutable. ALTER TABLE t ADD COLUMN z myval GENERATED ALWAYS AS (GREATEST(x, y)) STORED; -- Expected: ERROR: generation expression is not immutable -- Actual: ALTER TABLE (bug: accepted without error) The fix would be to add a MinMaxExpr case to contain_volatile_functions_walker that resolves the comparison operator's oprcode (analogous to the existing OpExpr handling) and checks its provolatile. --000000000000dd33110653416089 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
PostgreSQL version: 18.4 (also reproduced on 18.3)
Plat= form: aarch64-apple-darwin, Apple clang 17

GREATEST and LEAST (MinMa= xExpr nodes) are missing from contain_volatile_functions_walker in src/back= end/optimizer/util/clauses.c. OpExpr nodes are correctly handled -- the wal= ker resolves the oprcode via set_opfuncid and checks func_volatile. But the= re is no MinMaxExpr case, so GREATEST/LEAST always appears function-free to= the volatility checker regardless of the < or > operator's provo= latile.

The practical consequence: a GREATEST/LEAST expression whose= comparison operator is STABLE or VOLATILE is incorrectly treated as IMMUTA= BLE. The reproducer below demonstrates this via a generated column, which r= equires an IMMUTABLE expression. PostgreSQL accepts the column definition w= hen it should reject it.

-- Reproducer (self-contained, = tested on 18.4):

DROP TABLE IF EXISTS t CASCADE;
DROP TYPE= IF EXISTS myval CASCADE;

CREATE TYPE myval;

CREATE FUNCTION = myval_in(cstring) RETURNS myval
=C2=A0 =C2=A0 LANGUAGE internal STRICT I= MMUTABLE AS 'textin';
CREATE FUNCTION myval_out(myval) RETURNS c= string
=C2=A0 =C2=A0 LANGUAGE internal STRICT IMMUTABLE AS 'textout&= #39;;

CREATE TYPE myval (INPUT =3D myval_in, OUTPUT =3D myval_out, L= IKE =3D text);

-- < is STABLE: result depends on the myval.revers= e session GUC
CREATE FUNCTION myval_lt(a myval, b myval) RETURNS boolean=
=C2=A0 =C2=A0 LANGUAGE sql STABLE STRICT AS $$
=C2=A0 =C2=A0 =C2=A0 = =C2=A0 SELECT CASE current_setting('myval.reverse', true)
=C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0WHEN 'on' THEN a::= text > b::text
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0ELSE =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0a::text < b::text
= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0END
=C2=A0 =C2=A0= $$;

CREATE OPERATOR < (
=C2=A0 =C2=A0 leftarg =3D myval, righ= targ =3D myval,
=C2=A0 =C2=A0 procedure =3D myval_lt, negator =3D >= =3D
);

-- btree opclass so GREATEST can resolve a sort operator f= or myval
CREATE FUNCTION myval_cmp(a myval, b myval) RETURNS integer
= =C2=A0 =C2=A0 LANGUAGE sql IMMUTABLE STRICT AS $$
=C2=A0 =C2=A0 =C2=A0 = =C2=A0 SELECT CASE WHEN a::text < b::text THEN -1
=C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 WHEN a::text > b::t= ext THEN =C2=A01
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 ELSE 0 END
=C2=A0 =C2=A0 $$;

CREATE OPERATOR CLASS= myval_ops DEFAULT FOR TYPE myval USING btree AS
=C2=A0 =C2=A0 OPERATOR = 1 <,
=C2=A0 =C2=A0 FUNCTION 1 myval_cmp(myval, myval);

CREATE = TABLE t (x myval, y myval);

-- Control: a direct call to the Stable = function is correctly rejected.
-- ALTER TABLE t ADD COLUMN ctrl boolean=
-- =C2=A0 =C2=A0 GENERATED ALWAYS AS (myval_lt(x, y)) STORED;
-- =3D= > ERROR: generation expression is not immutable =C2=A0(correct)

-= - Bug: GREATEST uses the same Stable < operator but the missing
-- Mi= nMaxExpr case in contain_volatile_functions_walker means
-- PostgreSQL t= reats the expression as immutable.
ALTER TABLE t ADD COLUMN z myval
= =C2=A0 =C2=A0 GENERATED ALWAYS AS (GREATEST(x, y)) STORED;
-- Expected: = ERROR: generation expression is not immutable
-- Actual: =C2=A0 ALTER TA= BLE =C2=A0(bug: accepted without error)

The fix would be to add a Mi= nMaxExpr case to contain_volatile_functions_walker that resolves the compar= ison operator's oprcode (analogous to the existing OpExpr handling) and= checks its provolatile.
--000000000000dd33110653416089--