From aeb9511967b3eace8dc3b49aa6ffd583b0bfb9e9 Mon Sep 17 00:00:00 2001 From: Utkarash Kumar Singh Date: Thu, 27 Aug 2026 16:33:57 +0100 Subject: [PATCH] docs: retarget upgrade caveats to 15.19/17.11 + add btree_gist reindex note (#49621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Docs update — Postgres upgrade guide for the 15.19 / 17.11 minor release. ## What is the current behavior? The upgrade guide's caveats are tagged for 15.18 / 17.10. ## What is the new behavior? - Retarget the ltree-reindex and custom-operator caveats: `15.18 or 17.10` → `15.19 or 17.11`. - Replace the ltree detection query with an operator-class-based one that also catches expression indexes and excludes `INCLUDE`d columns. - Add a `btree_gist` caveat: `float4`/`float8` gist indexes that may contain `NaN` need a `REINDEX` (upstream fixed NaN handling in 15.19/17.11). - Note the separate ltree >~14,653-label overflow case (encoding-independent). - Use schema-qualified names in `REINDEX INDEX CONCURRENTLY` and note it cannot run inside a transaction block. Detection queries validated on real 15.19 and 17.11. ## Additional context Refs: PSQL-1245. A pgcrypto (CVE-2026-14663) caveat is intentionally not included here. ## Summary by CodeRabbit * **Documentation** * Updated upgrade guidance for PostgreSQL 15.19 and 17.11. * Documented schema-qualified ltree index names and transaction-block restrictions for reindexing. * Improved the ltree overflow query to account for partial-index predicates when identifying values exceeding approximately 14,653 labels. * Added guidance for identifying and concurrently rebuilding affected `btree_gist` floating-point indexes containing `NaN` values. * Updated supported-version guidance for custom operator selectivity estimators. --- .../content/guides/platform/upgrading.mdx | 120 +++++++++++++++--- 1 file changed, 104 insertions(+), 16 deletions(-) diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index 1400a906e59..13f873a7228 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -192,7 +192,7 @@ Existing projects on pg_graphql 1.5.x are not impacted unless they choose to upg ### Ltree indexes require reindexing after upgrade -_Applies when upgrading to Postgres 15.18 or 17.10._ +_Applies when upgrading to Postgres 15.19 or 17.11._ @@ -221,29 +221,115 @@ To mitigate this issue: 2. If `reindex_required` is `true`, find the affected indexes: ```sql - select schemaname, tablename, indexname - from pg_indexes + select distinct + n.nspname as schema_name, + cls.relname as table_name, + ic.relname as index_name + from pg_index idx + join pg_class ic on idx.indexrelid = ic.oid + join pg_class cls on idx.indrelid = cls.oid + join pg_namespace n on ic.relnamespace = n.oid + join lateral unnest(idx.indclass::oid[]) with ordinality as k(opclass, pos) on true + join pg_opclass oc on oc.oid = k.opclass + join pg_type ty on ty.oid = oc.opcintype where - indexname in ( - select c.relname - from - pg_index as i - join pg_class as c on i.indexrelid = c.oid - join pg_attribute as a on a.attrelid = i.indrelid and a.attnum = ANY(i.indkey) - join pg_type as t on a.atttypid = t.oid - where t.typname in ('ltree', '_ltree') - ); + k.pos <= idx.indnkeyatts -- key columns only, excludes INCLUDE + and ty.typname in ('ltree', '_ltree'); ``` -3. Reindex each affected index. `REINDEX INDEX CONCURRENTLY` runs online with no downtime: +3. Reindex each affected index using its schema-qualified name. `REINDEX INDEX CONCURRENTLY` runs online with no downtime, but cannot run inside a transaction block: ```sql - REINDEX INDEX CONCURRENTLY ; + REINDEX INDEX CONCURRENTLY .; + ``` + +Separately from the encoding case above, this release also fixes an integer overflow in `ltree` comparisons: values with more than about 14,653 labels could compare incorrectly, which can corrupt B-tree indexes built over them, regardless of your database encoding. The following query lists only the B-tree indexes whose `ltree` column or expression actually contains such values, so they are the ones to reindex (an empty result means no action is needed): + +```sql +SELECT s.schema_name || '.' || s.index_name AS index_to_reindex +FROM ( + SELECT + n.nspname AS schema_name, + c.relname AS table_name, + ic.relname AS index_name, + min(pg_get_expr(i.indpred, i.indrelid)) AS pred, -- partial-index predicate, if any + string_agg('nlevel(' || pg_get_indexdef(i.indexrelid, k.pos::int, true) || ') > 14653', ' OR ') AS keys_cond + FROM pg_index i + JOIN pg_class ic ON ic.oid = i.indexrelid + JOIN pg_class c ON c.oid = i.indrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_am am ON am.oid = ic.relam + JOIN LATERAL generate_series(1, i.indnkeyatts) AS k(pos) ON true + JOIN pg_attribute ia ON ia.attrelid = i.indexrelid AND ia.attnum = k.pos + JOIN pg_type t ON t.oid = ia.atttypid + WHERE am.amname = 'btree' + AND t.typname = 'ltree' + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + GROUP BY n.nspname, c.relname, ic.relname +) s +WHERE (xpath( + '/row/cnt/text()', + query_to_xml( + format('SELECT count(*) AS cnt FROM %I.%I WHERE %s(%s)', + s.schema_name, s.table_name, + CASE WHEN s.pred IS NOT NULL THEN '(' || s.pred || ') AND ' ELSE '' END, + s.keys_cond), + false, true, '' + ) + ))[1]::text::bigint > 0 +ORDER BY 1; +``` + +Reindex each index it returns, using the schema-qualified name. `REINDEX INDEX CONCURRENTLY` runs online with no downtime, but cannot run inside a transaction block: + +```sql +REINDEX INDEX CONCURRENTLY .; +``` + +### Btree_gist indexes on float columns require reindexing after upgrade + +_Applies when upgrading to Postgres 15.19 or 17.11._ + + + +You are affected only if you have `btree_gist` indexes on `float4` or `float8` columns that may contain `NaN` values. + + + +This release fixes `NaN` handling in `btree_gist`'s `float4` and `float8` operator classes. Indexes on those columns built under the previous version can return wrong results for rows containing `NaN` until the index is rebuilt. + +To mitigate this issue: + +1. Find `btree_gist` indexes on float columns: + + ```sql + select distinct + n.nspname as schema_name, + cls.relname as table_name, + ic.relname as index_name + from pg_index idx + join pg_class ic on idx.indexrelid = ic.oid + join pg_am am on ic.relam = am.oid + join pg_class cls on idx.indrelid = cls.oid + join pg_namespace n on ic.relnamespace = n.oid + join lateral unnest(idx.indclass::oid[]) with ordinality as k(opclass, pos) on true + join pg_opclass oc on oc.oid = k.opclass + join pg_type ty on ty.oid = oc.opcintype + where + am.amname = 'gist' + and k.pos <= idx.indnkeyatts + and ty.typname in ('float4', 'float8'); + ``` + +2. If any indexes are returned and those columns may contain `NaN` values, reindex them using the schema-qualified name. `REINDEX INDEX CONCURRENTLY` runs online with no downtime, but cannot run inside a transaction block: + + ```sql + REINDEX INDEX CONCURRENTLY .; ``` ### Custom operator selectivity estimators -_Applies when upgrading to Postgres 15.18 or 17.10._ +_Applies when upgrading to Postgres 15.19 or 17.11._ Attaching a non-built-in (extension- or user-provided) selectivity estimator function to an operator now requires superuser. Existing operators continue to work — the check only fires when an operator is (re)created, most commonly during `pg_dump` / `pg_restore`, a logical restore, or a branch. @@ -256,7 +342,9 @@ ERROR: must be superuser to specify a non-built-in restriction estimator functio Most projects are not affected. To check whether your database has any user-defined operators that reference a non-built-in estimator: ```sql -SELECT n.nspname AS schema, o.oprname AS operator +SELECT n.nspname AS schema, o.oprname AS operator, + o.oprrest::regproc AS restrict_estimator, + o.oprjoin::regproc AS join_estimator FROM pg_operator o JOIN pg_namespace n ON o.oprnamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')