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 1wN9DQ-000bwC-2Q for pgsql-bugs@arkaria.postgresql.org; Wed, 13 May 2026 13:01:32 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wN9DO-008tJT-2o for pgsql-bugs@arkaria.postgresql.org; Wed, 13 May 2026 13:01:31 +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 1wMrS6-005S4V-0m for pgsql-bugs@lists.postgresql.org; Tue, 12 May 2026 18:03:30 +0000 Received: from mail-yx1-xb132.google.com ([2607:f8b0:4864:20::b132]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wMrS3-00000000GUV-0B0a for pgsql-bugs@lists.postgresql.org; Tue, 12 May 2026 18:03:30 +0000 Received: by mail-yx1-xb132.google.com with SMTP id 956f58d0204a3-651d6347a69so5663817d50.0 for ; Tue, 12 May 2026 11:03:27 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1778609005; cv=none; d=google.com; s=arc-20240605; b=c6Corh4AqStUOhoGQ90HTy5oIxbRFu0NGUbJlyQsU448+zlwz2ftki2hCJt74ss9mo QP/NG5TupJYEE14emzcqy3KSZykrdjq9m0cvHRiyjWpnR4zQ3w+eaHFBGBkn0vF0uOVH RkvRMWH+6+7x9Wmbl+YNgqYCHQbky9Dss9F592EHHJOAn1UHnkgIfAKD/fUTI0Eaepaw AtWcOb4kPYEpdj/pBfB2SjS8mDuarrwkHiG6jniYNcJdF02kedxpRDZ3tskT/65cRwMy 9S0TAS90f2qR39Tu3Nb3pmuSY+YUkBFq4Uu/YVKXcoluX5y6HBEBl3B78w6/FDb+R0ro QSDA== 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=//tl06HywDrYizpM92Wu7xTMmMxo8P/nf6yF6V7VHkY=; fh=/gQe77b11iMZdcPj/nJr/Ghqi6rQp5FPrPbdO93bmOA=; b=kn5XxotxsAzmSZwyBZ/Oamyfb4YTtVbYsN6XYa2xpjDwDp8uhzY416Mip6GcJWbEmG Qiv1+CbbR0CYU7o0SkKVzwYa1gAmWNDfBBfD4ewRC2tTGrv2KksMXocDLWTgmSjsrrkC 1Y9nuMPpZjsDkmxPCPCSYmLGLCYMDkOrJ3gygPAR0ZhuojsNUOTHNMolylvJbteEE9ez i6rC6zaca6bfWIfPCW2tFpweJgTq+dNcazspU+WoS6p9mS7Tv6A5cYmatvhEy51xOVaq 74q2vBL/bD0eoOK8gX8FSvpGcIniBFG4gQogRY8tFV2rjZUmvgbCm8EA+WmUSSbwbxQV B/zQ==; 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=gmail.com; s=20251104; t=1778609005; x=1779213805; darn=lists.postgresql.org; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=//tl06HywDrYizpM92Wu7xTMmMxo8P/nf6yF6V7VHkY=; b=ITwq0OYdg1482DI//jFYSNO+0abTzKy3lySAsNBi8yEE++EZxU9I1Hg6E1nQ6aG8wZ pvlbGvsnW8ADoFy1/NiiK5FTQnRMQMfNw5RcaNF37TUKuTggMmJROwu1fXKO72qT/IP3 WJQBW2DknhxCGFREb3QFWIuC0EfIed5HKP6d8Seg7WbbKJtpU5m8QiuGSTfbAh/0YgUD 71Wa0sPJcW1mLbizofepBp4KR+LZ+lM4MLF2DhlfK4ONl4W78OeX4lRXGP+xBvw0iaQA 9N7ubz3CX5kMpUuQToCbcQnAaFTEY94rTMTNoQQySrlHdF6Nex6B/wou3g5Fb72gKgik lVMA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778609005; x=1779213805; 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=//tl06HywDrYizpM92Wu7xTMmMxo8P/nf6yF6V7VHkY=; b=IJ6fcQyJ/b7qq7r6ZC1rjrAB/TQfmPVzecMry+MjIfhqFN3+SVsl4WmkggG488HFBt 8Rt4fU9eJ9KgzVQN5dx3pfGcEqSt1qqLYdR7jrEDGo4kx9R/HP47o32tKBBMfsfBwY5A R6RTHEyGcDt6B/Cd19LTurnGzo/fYeBCnj1/b1gNEqxNj4MMfAEn/RXywew6sbSNNew4 2mM4KMvzmKOPGQbn8EejppSZzHydmFiAZeqUzAsQSIQ0hW1XEHsNk3PD9cNTYuiVlPAC IYouH0/lCNEBjfOatMIZOx1rzRzgDwoGVhslIiDsHAEtMxN6MhSLT04v/DBUdvQ8AfmW AIrQ== X-Gm-Message-State: AOJu0YyAMOouajY9xpOSiKpFKppH5aLdfiaaYS+ayz0RtEp0+2kHH/fl ihmXi5QUn1d6UE5vzSnrpAPAJ+K0tyi9nDTMwL6Wq8/z5qq2ySiK/PIBF25EMmv4ISPJCOk5cgF ft4rQxb8CyCZYyqKMqqKQvrcKUbX0bcPuYii9/aE= X-Gm-Gg: Acq92OG/yI2EPxwq42dqfkh6mcQ0iosEdhJ9OHQdGuZe2smC+l7WzIbINkT/K7dRvgp Ym+TuL9+c5Y69csRrQ+hpSI7QgZdDDOYG1yXich9JnqE5VoKxp09BcgVSFwuOZMpw7jB/B5S/21 2bXl/pLGovGzBS4lkZJuNLJXCMQ5tO4yOyCs6HjO/gypnwprzWe7IJlsJBeglRfiKCuM4MUXa95 Sx4b5u5Yh+PhOZokRmECRrRlj01UWQGI3WsQdyX0FiNkQOIsbUGONilE+6J7wbql04qmkrNn8jt h0oYWTIvdaIkBltiaGp+ssKBGsu/Gre9edQORXwekXVpiahse4HKmO37RoE3f7sAAO2RVPjXVND kPnbdCO0= X-Received: by 2002:a05:690e:400b:b0:650:14bd:f344 with SMTP id 956f58d0204a3-65df5ea39d9mr88881d50.14.1778609005242; Tue, 12 May 2026 11:03:25 -0700 (PDT) MIME-Version: 1.0 From: Marcelo Lauxen Date: Tue, 12 May 2026 15:03:14 -0300 X-Gm-Features: AVHnY4K6VoeNEM9gncnL_s06EljFcr4iSDUwhwx4U62d7gSe9iYIott_0Kq-9V4 Message-ID: Subject: =?UTF-8?Q?pg=5Fget=5Findexdef=28=29_output_not_idempotent_for_partial_?= =?UTF-8?Q?indexes_with_ALL=28ARRAY=5B=E2=80=A6=5D=29=3A=3Atext=5B=5D?= To: pgsql-bugs@lists.postgresql.org Content-Type: multipart/alternative; boundary="000000000000f3af510651a2ad4e" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --000000000000f3af510651a2ad4e Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable *PostgreSQL version*: 18.3 (Homebrew) on aarch64-apple-darwin24.6.0 *pg_get_indexdef()* produces SQL that, when executed, yields a different pg_get_indexdef() output. This means a pg_dump =E2=86=92 pg_restore cycle s= ilently changes the deparsed form of partial index WHERE clauses that use NOT IN (...) on a varchar column, causing cosmetic drift in tools that compare index definitions (e.g. ORM schema dumps, annotation generators). *Reproduction:* > -- Setup > CREATE TABLE test_idempotent ( > id bigint PRIMARY KEY, > state varchar NOT NULL, > space_id bigint > ); > > -- Step 1: Create index using NOT IN > CREATE UNIQUE INDEX idx_original ON test_idempotent (space_id) > WHERE state NOT IN ('completed', 'failed', 'cancelled') > AND space_id IS NOT NULL; > > -- Step 2: Capture pg_get_indexdef output > SELECT pg_get_indexdef(indexrelid) AS pass_1 > FROM pg_stat_user_indexes > WHERE indexrelname =3D 'idx_original'; > > -- pass_1 result: > -- CREATE UNIQUE INDEX idx_original ON public.test_idempotent USING btree > (space_id) > -- WHERE (((state)::text <> ALL ((ARRAY['completed'::character varying, > -- 'failed'::character varying, 'cancelled'::character > varying])::text[])) > -- AND (space_id IS NOT NULL)) > > -- Step 3: Feed pass_1 output back as a new index > CREATE UNIQUE INDEX idx_round_trip ON public.test_idempotent USING btree > (space_id) > WHERE (((state)::text <> ALL ((ARRAY['completed'::character varying, > 'failed'::character varying, 'cancelled'::character varying])::text[]= )) > AND (space_id IS NOT NULL)); > > -- Step 4: Compare both > SELECT indexrelname, pg_get_indexdef(indexrelid) > FROM pg_stat_user_indexes > WHERE indexrelname IN ('idx_original', 'idx_round_trip') > ORDER BY indexrelname; > > -- Cleanup > DROP TABLE test_idempotent; *Observed result (step 4):* > idx_original | ... WHERE (((state)::text <> ALL > ((ARRAY['completed'::character varying, 'failed'::character varying, > 'cancelled'::character varying])::text[])) AND (space_id IS NOT NULL)) idx_round_trip | ... WHERE (((state)::text <> ALL > (ARRAY[('completed'::character varying)::text, ('failed'::character > varying)::text, ('cancelled'::character varying)::text])) AND (space_id I= S > NOT NULL)) The array-level cast (ARRAY[...])::text[] is redistributed to per-element casts ARRAY[(...::text), (...::text), (...::text)] after a single parse=E2=86=92deparse cycle. *Expected result:* Both indexes should produce identical pg_get_indexdef() output since idx_round_trip was created using the exact SQL that pg_get_indexdef() produced for idx_original. --000000000000f3af510651a2ad4e Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
PostgreSQL version: 18.3 (Homebrew) on aarch64-apple-darwin24.6.0

pg_get_indexdef() produces SQL that, when execut= ed, yields a different pg_get_indexdef() output. This means a pg_dump =E2= =86=92 pg_restore cycle silently changes the deparsed form of partial index WHERE clauses that use NOT IN (...) on a varchar c= olumn, causing cosmetic drift in tools that compare index definitions (e.g.= ORM schema dumps, annotation generators).

Repr= oduction:
-- Set= up
CREATE TABLE test_idempotent (
=C2=A0 id bigint PRIMARY KEY,
= =C2=A0 state varchar NOT NULL,
=C2=A0 space_id bigint
);

-- St= ep 1: Create index using NOT IN
CREATE UNIQUE INDEX idx_original ON test= _idempotent (space_id)
=C2=A0 WHERE state NOT IN ('completed', &= #39;failed', 'cancelled')
=C2=A0 =C2=A0 AND space_id IS NOT = NULL;

-- Step 2: Capture pg_get_indexdef output
SELECT pg_get_ind= exdef(indexrelid) AS pass_1
=C2=A0 FROM pg_stat_user_indexes
=C2=A0 W= HERE indexrelname =3D 'idx_original';

-- pass_1 result:
-= - CREATE UNIQUE INDEX idx_original ON public.test_idempotent USING btree (s= pace_id)
-- =C2=A0 WHERE (((state)::text <> ALL ((ARRAY['compl= eted'::character varying,
-- =C2=A0 =C2=A0 'failed'::charact= er varying, 'cancelled'::character varying])::text[]))
-- =C2=A0= =C2=A0 AND (space_id IS NOT NULL))

-- Step 3: Feed pass_1 output ba= ck as a new index
CREATE UNIQUE INDEX idx_round_trip ON public.test_idem= potent USING btree (space_id)
=C2=A0 WHERE (((state)::text <> ALL = ((ARRAY['completed'::character varying,
=C2=A0 =C2=A0 'faile= d'::character varying, 'cancelled'::character varying])::text[]= ))
=C2=A0 =C2=A0 AND (space_id IS NOT NULL));

-- Step 4: Compare = both
SELECT indexrelname, pg_get_indexdef(indexrelid)
=C2=A0 FROM pg_= stat_user_indexes
=C2=A0 WHERE indexrelname IN ('idx_original', = 'idx_round_trip')
=C2=A0 ORDER BY indexrelname;

-- Cleanu= p
DROP TABLE test_idempotent;

Observe= d result (step 4):
idx_original =C2=A0 | ... WHERE (((state)::text <> ALL ((ARRAY[= 9;completed'::character varying, 'failed'::character varying, &= #39;cancelled'::character varying])::text[])) AND (space_id IS NOT NULL= ))
=C2=A0idx_= round_trip | ... WHERE (((state)::text <> ALL (ARRAY[('completed&= #39;::character varying)::text, ('failed'::character varying)::text= , ('cancelled'::character varying)::text])) AND (space_id IS NOT NU= LL))=C2=A0

The array-level cast (ARRAY[...])::text[] i= s redistributed to per-element casts ARRAY[(...::text), (...::text), (...::= text)] after a single parse=E2=86=92deparse cycle.

Expected resul= t:

Both indexes should produce identical pg_get_indexdef() outpu= t since idx_round_trip was created using the exact SQL that pg_get_indexdef= () produced for idx_original.=C2=A0
--000000000000f3af510651a2ad4e--