Skip to content

Commit b280ca0

Browse files
committed
Add pg_ensure_direct_toast for in-place legacy TOAST table upgrade
When a database cluster is upgraded from a pre-Direct-TOAST PostgreSQL version or when an existing table with a legacy 3-column TOAST table is switched to Direct TOAST, the TOAST table may lack the 'chunk_tids' and 'chunk_tid_offsets' columns and the partial index predicate. This commit introduces: 1. ensure_direct_toast(Oid relid): Performs in-place catalog metadata upgrades to add the missing columns in pg_attribute, updates pg_class.relnatts = 5, and marks the TOAST index as partial with WHERE chunk_id IS NOT NULL in pg_index. 2. pg_ensure_direct_toast(regclass): SQL function callable by DBAs. 3. Automatic upgrade hook in ATExecSetRelOptions() when setting toast_flavour = 'direct'. 4. Write-time guard in toast_save_datum_direct() rejecting writes to legacy 3-column TOAST tables with an informative hint. 5. Regression test suite and documentation updates.
1 parent 1046e0d commit b280ca0

8 files changed

Lines changed: 479 additions & 0 deletions

File tree

‎doc/src/sgml/storage.sgml‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,86 @@ Furthermore, <literal>plain</literal> and <literal>direct</literal> <acronym>TOA
484484
pointers can seamlessly coexist within the same table, allowing tables upgraded from
485485
older <productname>PostgreSQL</productname> versions to be read transparently and
486486
gradually adopt Direct <acronym>TOAST</acronym> for new writes.
487+
When upgrading a pre-existing table that was created prior to Direct <acronym>TOAST</acronym>
488+
support (having a legacy 3-column <acronym>TOAST</acronym> table), executing
489+
<command>ALTER TABLE ... SET (toast_flavour = 'direct')</command> automatically
490+
upgrades the <acronym>TOAST</acronym> table in-place to the 5-column format with a
491+
partial index. Administrators can also explicitly upgrade any table or <acronym>TOAST</acronym>
492+
table by calling <function>pg_ensure_direct_toast(<type>regclass</type>)</function>.
493+
</para>
494+
495+
<para>
496+
To upgrade all user tables' <acronym>TOAST</acronym> tables in a quiet database in bulk,
497+
the following query can be used:
498+
<programlisting>
499+
SELECT c.oid::regclass AS tablename,
500+
pg_ensure_direct_toast(c.reltoastrelid::regclass)
501+
FROM pg_class AS c
502+
WHERE c.reltoastrelid != 0
503+
AND EXISTS (SELECT FROM pg_stat_user_tables AS t WHERE t.relid = c.oid);
504+
</programlisting>
505+
</para>
506+
507+
<para>
508+
For a busy production database, tables can be upgraded one by one with lock timeouts
509+
and growing wait times between retries to avoid lock contention:
510+
<programlisting>
511+
DO $$
512+
DECLARE
513+
lock_timeout_ms int := 100;
514+
max_attempts int := 10;
515+
tablename text;
516+
toastoid regclass;
517+
attempt_nr int;
518+
update_completed boolean;
519+
attempts text;
520+
any_found boolean := false;
521+
BEGIN
522+
PERFORM set_config('lock_timeout', lock_timeout_ms || 'ms', false);
523+
524+
FOR tablename, toastoid IN
525+
SELECT c.oid::regclass::text,
526+
c.reltoastrelid::regclass
527+
FROM pg_class AS c
528+
WHERE EXISTS (SELECT FROM pg_stat_user_tables AS t WHERE t.relid = c.oid)
529+
AND (SELECT count(*)
530+
FROM pg_attribute AS a
531+
WHERE a.attrelid = c.reltoastrelid) = 3
532+
LOOP
533+
any_found := true;
534+
update_completed := false;
535+
FOR attempt_nr IN 1..max_attempts LOOP
536+
BEGIN
537+
PERFORM pg_ensure_direct_toast(toastoid);
538+
update_completed := true;
539+
EXIT;
540+
EXCEPTION
541+
WHEN lock_not_available THEN
542+
PERFORM pg_sleep(0.1 * attempt_nr); /* sleep a little longer each time */
543+
WHEN OTHERS THEN
544+
RAISE WARNING 'Error updating % to Direct TOAST: %', tablename, SQLERRM;
545+
EXIT;
546+
END;
547+
END LOOP;
548+
549+
IF update_completed THEN
550+
IF attempt_nr > 1 THEN
551+
attempts := format(' after %s attempts', attempt_nr);
552+
ELSE
553+
attempts := '';
554+
END IF;
555+
RAISE INFO 'Table % updated to Direct TOAST%', tablename, attempts;
556+
ELSE
557+
RAISE WARNING 'Timeout waiting to update table % to Direct TOAST', tablename;
558+
END IF;
559+
END LOOP;
560+
561+
IF NOT any_found THEN
562+
RAISE INFO 'No tables needed update to use Direct TOAST';
563+
END IF;
564+
END;
565+
$$;
566+
</programlisting>
487567
</para>
488568

489569
<para>

‎src/backend/access/common/toast_internals.c‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,15 @@ toast_save_datum_direct(Relation rel, Datum value,
996996
state.chunk_seq = 0;
997997
state.max_chunk_size = TOAST_MAX_CHUNK_SIZE(TupleDescAttr(state.toasttupDesc, 0)->atttypid);
998998

999+
if (state.toasttupDesc->natts < 5)
1000+
ereport(ERROR,
1001+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
1002+
errmsg("cannot write direct TOAST datum to legacy TOAST table \"%s\"",
1003+
RelationGetRelationName(state.toastrel)),
1004+
errhint("Run \"SELECT pg_ensure_direct_toast('%s'::regclass);\" or \"ALTER TABLE %s SET (toast_flavour = 'direct');\" to upgrade the TOAST table.",
1005+
RelationGetRelationName(rel),
1006+
RelationGetRelationName(rel))));
1007+
9991008
if (VARATT_IS_SHORT(dval))
10001009
{
10011010
data_p = VARDATA_SHORT(dval);

‎src/backend/catalog/toasting.c‎

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@
2727
#include "catalog/pg_am.h"
2828
#include "catalog/pg_namespace.h"
2929
#include "catalog/pg_opclass.h"
30+
#include "catalog/pg_type.h"
3031
#include "catalog/toasting.h"
3132
#include "miscadmin.h"
3233
#include "nodes/makefuncs.h"
34+
#include "nodes/nodeFuncs.h"
35+
#include "utils/builtins.h"
3336
#include "utils/fmgroids.h"
37+
#include "utils/inval.h"
38+
#include "utils/lsyscache.h"
3439
#include "utils/rel.h"
3540
#include "utils/lsyscache.h"
3641
#include "utils/syscache.h"
@@ -514,3 +519,176 @@ needs_toast_table(Relation rel)
514519
/* Otherwise, let the AM decide. */
515520
return table_relation_needs_toast_table(rel);
516521
}
522+
523+
/*
524+
* ensure_direct_toast
525+
*
526+
* Upgrades a legacy (3-column, full index) TOAST table in-place to support
527+
* the Direct TOAST format.
528+
* Accepts either the parent table's OID or the TOAST table's OID.
529+
*/
530+
void
531+
ensure_direct_toast(Oid relid)
532+
{
533+
Relation targetrel;
534+
Relation toastrel;
535+
Oid toastrelid;
536+
Relation pg_class_rel;
537+
Relation pg_attribute_rel;
538+
Relation pg_index_rel;
539+
HeapTuple tuple;
540+
HeapTuple newtuple;
541+
TupleDesc td;
542+
Oid toastIndexOid = InvalidOid;
543+
List *indexoids;
544+
545+
targetrel = table_open(relid, AccessShareLock);
546+
547+
if (targetrel->rd_rel->relkind == RELKIND_TOASTVALUE)
548+
{
549+
toastrelid = relid;
550+
table_close(targetrel, AccessShareLock);
551+
}
552+
else if (targetrel->rd_rel->relkind == RELKIND_RELATION ||
553+
targetrel->rd_rel->relkind == RELKIND_MATVIEW)
554+
{
555+
toastrelid = targetrel->rd_rel->reltoastrelid;
556+
table_close(targetrel, AccessShareLock);
557+
558+
if (!OidIsValid(toastrelid))
559+
ereport(ERROR,
560+
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
561+
errmsg("table \"%s\" does not have a TOAST table",
562+
get_rel_name(relid))));
563+
}
564+
else
565+
{
566+
table_close(targetrel, AccessShareLock);
567+
ereport(ERROR,
568+
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
569+
errmsg("relation \"%s\" is not a table or TOAST table",
570+
get_rel_name(relid))));
571+
}
572+
573+
/* Open the toast relation with exclusive lock */
574+
toastrel = table_open(toastrelid, AccessExclusiveLock);
575+
576+
if (toastrel->rd_rel->relkind != RELKIND_TOASTVALUE)
577+
elog(ERROR, "relation %u is not a TOAST table", toastrelid);
578+
579+
/*
580+
* Step 1: Add missing columns chunk_tids and chunk_tid_offsets if needed.
581+
*/
582+
if (toastrel->rd_att->natts < 5)
583+
{
584+
td = CreateTemplateTupleDesc(2);
585+
TupleDescInitEntry(td, (AttrNumber) 1,
586+
"chunk_tids",
587+
TIDARRAYOID,
588+
-1, 0);
589+
TupleDescInitEntry(td, (AttrNumber) 2,
590+
"chunk_tid_offsets",
591+
INT8ARRAYOID,
592+
-1, 0);
593+
TupleDescAttr(td, 0)->attnum = 4;
594+
TupleDescAttr(td, 0)->attstorage = TYPSTORAGE_PLAIN;
595+
TupleDescAttr(td, 0)->attcompression = InvalidCompressionMethod;
596+
TupleDescAttr(td, 1)->attnum = 5;
597+
TupleDescAttr(td, 1)->attstorage = TYPSTORAGE_PLAIN;
598+
TupleDescAttr(td, 1)->attcompression = InvalidCompressionMethod;
599+
600+
populate_compact_attribute(td, 0);
601+
populate_compact_attribute(td, 1);
602+
TupleDescFinalize(td);
603+
604+
pg_attribute_rel = table_open(AttributeRelationId, RowExclusiveLock);
605+
InsertPgAttributeTuples(pg_attribute_rel, td, toastrelid, NULL, NULL);
606+
table_close(pg_attribute_rel, RowExclusiveLock);
607+
FreeTupleDesc(td);
608+
609+
/* Update pg_class.relnatts = 5 */
610+
pg_class_rel = table_open(RelationRelationId, RowExclusiveLock);
611+
tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(toastrelid));
612+
if (!HeapTupleIsValid(tuple))
613+
elog(ERROR, "cache lookup failed for relation %u", toastrelid);
614+
((Form_pg_class) GETSTRUCT(tuple))->relnatts = 5;
615+
CatalogTupleUpdate(pg_class_rel, &tuple->t_self, tuple);
616+
heap_freetuple(tuple);
617+
table_close(pg_class_rel, RowExclusiveLock);
618+
}
619+
620+
/*
621+
* Step 2: Ensure the TOAST unique index is partial (WHERE chunk_id IS NOT NULL).
622+
*/
623+
indexoids = RelationGetIndexList(toastrel);
624+
if (indexoids != NIL)
625+
toastIndexOid = linitial_oid(indexoids);
626+
list_free(indexoids);
627+
628+
if (OidIsValid(toastIndexOid))
629+
{
630+
pg_index_rel = table_open(IndexRelationId, RowExclusiveLock);
631+
tuple = SearchSysCacheCopy1(INDEXRELID, ObjectIdGetDatum(toastIndexOid));
632+
if (HeapTupleIsValid(tuple))
633+
{
634+
bool isnull;
635+
636+
(void) SysCacheGetAttr(INDEXRELID, tuple, Anum_pg_index_indpred, &isnull);
637+
638+
if (isnull)
639+
{
640+
Datum values[Natts_pg_index];
641+
bool nulls[Natts_pg_index];
642+
bool replaces[Natts_pg_index];
643+
NullTest *ntest;
644+
char *pred_str;
645+
646+
memset(values, 0, sizeof(values));
647+
memset(nulls, 0, sizeof(nulls));
648+
memset(replaces, 0, sizeof(replaces));
649+
650+
ntest = makeNode(NullTest);
651+
ntest->arg = (Expr *) makeVar(1, 1, OIDOID, -1, InvalidOid, 0);
652+
ntest->nulltesttype = IS_NOT_NULL;
653+
ntest->argisrow = false;
654+
ntest->location = -1;
655+
656+
pred_str = nodeToString(list_make1(ntest));
657+
658+
values[Anum_pg_index_indisprimary - 1] = BoolGetDatum(false);
659+
replaces[Anum_pg_index_indisprimary - 1] = true;
660+
661+
values[Anum_pg_index_indpred - 1] = CStringGetTextDatum(pred_str);
662+
replaces[Anum_pg_index_indpred - 1] = true;
663+
nulls[Anum_pg_index_indpred - 1] = false;
664+
665+
newtuple = heap_modify_tuple(tuple, RelationGetDescr(pg_index_rel),
666+
values, nulls, replaces);
667+
CatalogTupleUpdate(pg_index_rel, &tuple->t_self, newtuple);
668+
heap_freetuple(newtuple);
669+
pfree(pred_str);
670+
671+
CacheInvalidateRelcacheByRelid(toastIndexOid);
672+
}
673+
heap_freetuple(tuple);
674+
}
675+
table_close(pg_index_rel, RowExclusiveLock);
676+
}
677+
678+
CacheInvalidateRelcacheByRelid(toastrelid);
679+
CommandCounterIncrement();
680+
681+
table_close(toastrel, AccessExclusiveLock);
682+
}
683+
684+
/*
685+
* SQL-callable function
686+
*/
687+
Datum
688+
pg_ensure_direct_toast(PG_FUNCTION_ARGS)
689+
{
690+
Oid relid = PG_GETARG_OID(0);
691+
692+
ensure_direct_toast(relid);
693+
PG_RETURN_VOID();
694+
}

‎src/backend/commands/tablecmds.c‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17443,6 +17443,19 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
1744317443

1744417444
ReleaseSysCache(tuple);
1744517445

17446+
/* If toast_flavour is being set to 'direct', ensure the TOAST table supports it */
17447+
if (OidIsValid(rel->rd_rel->reltoastrelid) && newOptions != (Datum) 0 &&
17448+
(rel->rd_rel->relkind == RELKIND_RELATION || rel->rd_rel->relkind == RELKIND_MATVIEW))
17449+
{
17450+
StdRdOptions *opts = (StdRdOptions *) heap_reloptions(rel->rd_rel->relkind, newOptions, false);
17451+
17452+
if (opts && opts->toast_flavour == TOAST_FLAVOUR_DIRECT)
17453+
ensure_direct_toast(rel->rd_rel->reltoastrelid);
17454+
17455+
if (opts)
17456+
pfree(opts);
17457+
}
17458+
1744617459
/* repeat the whole exercise for the toast table, if there's one */
1744717460
if (OidIsValid(rel->rd_rel->reltoastrelid))
1744817461
{

‎src/include/catalog/pg_proc.dat‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12778,4 +12778,8 @@
1277812778
proname => 'hashoid8extended', prorettype => 'int8',
1277912779
proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
1278012780

12781+
{ oid => '9225', descr => 'ensure a TOAST table supports Direct TOAST format',
12782+
proname => 'pg_ensure_direct_toast', provolatile => 'v', prorettype => 'void',
12783+
proargtypes => 'regclass', prosrc => 'pg_ensure_direct_toast' },
12784+
1278112785
]

‎src/include/catalog/toasting.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ extern void AlterTableCreateToastTable(Oid relOid, Datum reloptions,
2626
LOCKMODE lockmode);
2727
extern void BootstrapToastTable(char *relName,
2828
Oid toastOid, Oid toastIndexOid);
29+
extern void ensure_direct_toast(Oid relid);
2930

3031
#endif /* TOASTING_H */

0 commit comments

Comments
 (0)