Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * toast_internals.c
4 : * Functions for internal use by the TOAST system.
5 : *
6 : * Copyright (c) 2000-2025, PostgreSQL Global Development Group
7 : *
8 : * IDENTIFICATION
9 : * src/backend/access/common/toast_internals.c
10 : *
11 : *-------------------------------------------------------------------------
12 : */
13 :
14 : #include "postgres.h"
15 :
16 : #include "access/detoast.h"
17 : #include "access/genam.h"
18 : #include "access/heapam.h"
19 : #include "access/heaptoast.h"
20 : #include "access/table.h"
21 : #include "access/toast_internals.h"
22 : #include "access/xact.h"
23 : #include "catalog/catalog.h"
24 : #include "miscadmin.h"
25 : #include "utils/fmgroids.h"
26 : #include "utils/rel.h"
27 : #include "utils/snapmgr.h"
28 :
29 : static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
30 : static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
31 :
32 : /* ----------
33 : * toast_compress_datum -
34 : *
35 : * Create a compressed version of a varlena datum
36 : *
37 : * If we fail (ie, compressed result is actually bigger than original)
38 : * then return NULL. We must not use compressed data if it'd expand
39 : * the tuple!
40 : *
41 : * We use VAR{SIZE,DATA}_ANY so we can handle short varlenas here without
42 : * copying them. But we can't handle external or compressed datums.
43 : * ----------
44 : */
45 : Datum
46 47734 : toast_compress_datum(Datum value, char cmethod)
47 : {
48 47734 : struct varlena *tmp = NULL;
49 : int32 valsize;
50 47734 : ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID;
51 :
52 : Assert(!VARATT_IS_EXTERNAL(DatumGetPointer(value)));
53 : Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(value)));
54 :
55 47734 : valsize = VARSIZE_ANY_EXHDR(DatumGetPointer(value));
56 :
57 : /* If the compression method is not valid, use the current default */
58 47734 : if (!CompressionMethodIsValid(cmethod))
59 47662 : cmethod = default_toast_compression;
60 :
61 : /*
62 : * Call appropriate compression routine for the compression method.
63 : */
64 47734 : switch (cmethod)
65 : {
66 47698 : case TOAST_PGLZ_COMPRESSION:
67 47698 : tmp = pglz_compress_datum((const struct varlena *) value);
68 47698 : cmid = TOAST_PGLZ_COMPRESSION_ID;
69 47698 : break;
70 36 : case TOAST_LZ4_COMPRESSION:
71 36 : tmp = lz4_compress_datum((const struct varlena *) value);
72 36 : cmid = TOAST_LZ4_COMPRESSION_ID;
73 36 : break;
74 0 : default:
75 0 : elog(ERROR, "invalid compression method %c", cmethod);
76 : }
77 :
78 47734 : if (tmp == NULL)
79 12126 : return PointerGetDatum(NULL);
80 :
81 : /*
82 : * We recheck the actual size even if compression reports success, because
83 : * it might be satisfied with having saved as little as one byte in the
84 : * compressed data --- which could turn into a net loss once you consider
85 : * header and alignment padding. Worst case, the compressed format might
86 : * require three padding bytes (plus header, which is included in
87 : * VARSIZE(tmp)), whereas the uncompressed format would take only one
88 : * header byte and no padding if the value is short enough. So we insist
89 : * on a savings of more than 2 bytes to ensure we have a gain.
90 : */
91 35608 : if (VARSIZE(tmp) < valsize - 2)
92 : {
93 : /* successful compression */
94 : Assert(cmid != TOAST_INVALID_COMPRESSION_ID);
95 35608 : TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid);
96 35608 : return PointerGetDatum(tmp);
97 : }
98 : else
99 : {
100 : /* incompressible data */
101 0 : pfree(tmp);
102 0 : return PointerGetDatum(NULL);
103 : }
104 : }
105 :
106 : /* ----------
107 : * toast_save_datum -
108 : *
109 : * Save one single datum into the secondary relation and return
110 : * a Datum reference for it.
111 : *
112 : * rel: the main relation we're working with (not the toast rel!)
113 : * value: datum to be pushed to toast storage
114 : * oldexternal: if not NULL, toast pointer previously representing the datum
115 : * options: options to be passed to heap_insert() for toast rows
116 : * ----------
117 : */
118 : Datum
119 15336 : toast_save_datum(Relation rel, Datum value,
120 : struct varlena *oldexternal, int options)
121 : {
122 : Relation toastrel;
123 : Relation *toastidxs;
124 : HeapTuple toasttup;
125 : TupleDesc toasttupDesc;
126 : Datum t_values[3];
127 : bool t_isnull[3];
128 15336 : CommandId mycid = GetCurrentCommandId(true);
129 : struct varlena *result;
130 : struct varatt_external toast_pointer;
131 : union
132 : {
133 : struct varlena hdr;
134 : /* this is to make the union big enough for a chunk: */
135 : char data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ];
136 : /* ensure union is aligned well enough: */
137 : int32 align_it;
138 : } chunk_data;
139 : int32 chunk_size;
140 15336 : int32 chunk_seq = 0;
141 : char *data_p;
142 : int32 data_todo;
143 15336 : Pointer dval = DatumGetPointer(value);
144 : int num_indexes;
145 : int validIndex;
146 :
147 : Assert(!VARATT_IS_EXTERNAL(value));
148 :
149 : /*
150 : * Open the toast relation and its indexes. We can use the index to check
151 : * uniqueness of the OID we assign to the toasted item, even though it has
152 : * additional columns besides OID.
153 : */
154 15336 : toastrel = table_open(rel->rd_rel->reltoastrelid, RowExclusiveLock);
155 15336 : toasttupDesc = toastrel->rd_att;
156 :
157 : /* Open all the toast indexes and look for the valid one */
158 15336 : validIndex = toast_open_indexes(toastrel,
159 : RowExclusiveLock,
160 : &toastidxs,
161 : &num_indexes);
162 :
163 : /*
164 : * Get the data pointer and length, and compute va_rawsize and va_extinfo.
165 : *
166 : * va_rawsize is the size of the equivalent fully uncompressed datum, so
167 : * we have to adjust for short headers.
168 : *
169 : * va_extinfo stored the actual size of the data payload in the toast
170 : * records and the compression method in first 2 bits if data is
171 : * compressed.
172 : */
173 15336 : if (VARATT_IS_SHORT(dval))
174 : {
175 0 : data_p = VARDATA_SHORT(dval);
176 0 : data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
177 0 : toast_pointer.va_rawsize = data_todo + VARHDRSZ; /* as if not short */
178 0 : toast_pointer.va_extinfo = data_todo;
179 : }
180 15336 : else if (VARATT_IS_COMPRESSED(dval))
181 : {
182 9120 : data_p = VARDATA(dval);
183 9120 : data_todo = VARSIZE(dval) - VARHDRSZ;
184 : /* rawsize in a compressed datum is just the size of the payload */
185 9120 : toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
186 :
187 : /* set external size and compression method */
188 9120 : VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
189 : VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
190 : /* Assert that the numbers look like it's compressed */
191 : Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
192 : }
193 : else
194 : {
195 6216 : data_p = VARDATA(dval);
196 6216 : data_todo = VARSIZE(dval) - VARHDRSZ;
197 6216 : toast_pointer.va_rawsize = VARSIZE(dval);
198 6216 : toast_pointer.va_extinfo = data_todo;
199 : }
200 :
201 : /*
202 : * Insert the correct table OID into the result TOAST pointer.
203 : *
204 : * Normally this is the actual OID of the target toast table, but during
205 : * table-rewriting operations such as CLUSTER, we have to insert the OID
206 : * of the table's real permanent toast table instead. rd_toastoid is set
207 : * if we have to substitute such an OID.
208 : */
209 15336 : if (OidIsValid(rel->rd_toastoid))
210 580 : toast_pointer.va_toastrelid = rel->rd_toastoid;
211 : else
212 14756 : toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
213 :
214 : /*
215 : * Choose an OID to use as the value ID for this toast value.
216 : *
217 : * Normally we just choose an unused OID within the toast table. But
218 : * during table-rewriting operations where we are preserving an existing
219 : * toast table OID, we want to preserve toast value OIDs too. So, if
220 : * rd_toastoid is set and we had a prior external value from that same
221 : * toast table, re-use its value ID. If we didn't have a prior external
222 : * value (which is a corner case, but possible if the table's attstorage
223 : * options have been changed), we have to pick a value ID that doesn't
224 : * conflict with either new or existing toast value OIDs.
225 : */
226 15336 : if (!OidIsValid(rel->rd_toastoid))
227 : {
228 : /* normal case: just choose an unused OID */
229 14756 : toast_pointer.va_valueid =
230 14756 : GetNewOidWithIndex(toastrel,
231 14756 : RelationGetRelid(toastidxs[validIndex]),
232 : (AttrNumber) 1);
233 : }
234 : else
235 : {
236 : /* rewrite case: check to see if value was in old toast table */
237 580 : toast_pointer.va_valueid = InvalidOid;
238 580 : if (oldexternal != NULL)
239 : {
240 : struct varatt_external old_toast_pointer;
241 :
242 : Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
243 : /* Must copy to access aligned fields */
244 580 : VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
245 580 : if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
246 : {
247 : /* This value came from the old toast table; reuse its OID */
248 580 : toast_pointer.va_valueid = old_toast_pointer.va_valueid;
249 :
250 : /*
251 : * There is a corner case here: the table rewrite might have
252 : * to copy both live and recently-dead versions of a row, and
253 : * those versions could easily reference the same toast value.
254 : * When we copy the second or later version of such a row,
255 : * reusing the OID will mean we select an OID that's already
256 : * in the new toast table. Check for that, and if so, just
257 : * fall through without writing the data again.
258 : *
259 : * While annoying and ugly-looking, this is a good thing
260 : * because it ensures that we wind up with only one copy of
261 : * the toast value when there is only one copy in the old
262 : * toast table. Before we detected this case, we'd have made
263 : * multiple copies, wasting space; and what's worse, the
264 : * copies belonging to already-deleted heap tuples would not
265 : * be reclaimed by VACUUM.
266 : */
267 580 : if (toastrel_valueid_exists(toastrel,
268 : toast_pointer.va_valueid))
269 : {
270 : /* Match, so short-circuit the data storage loop below */
271 0 : data_todo = 0;
272 : }
273 : }
274 : }
275 580 : if (toast_pointer.va_valueid == InvalidOid)
276 : {
277 : /*
278 : * new value; must choose an OID that doesn't conflict in either
279 : * old or new toast table
280 : */
281 : do
282 : {
283 0 : toast_pointer.va_valueid =
284 0 : GetNewOidWithIndex(toastrel,
285 0 : RelationGetRelid(toastidxs[validIndex]),
286 : (AttrNumber) 1);
287 0 : } while (toastid_valueid_exists(rel->rd_toastoid,
288 0 : toast_pointer.va_valueid));
289 : }
290 : }
291 :
292 : /*
293 : * Initialize constant parts of the tuple data
294 : */
295 15336 : t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
296 15336 : t_values[2] = PointerGetDatum(&chunk_data);
297 15336 : t_isnull[0] = false;
298 15336 : t_isnull[1] = false;
299 15336 : t_isnull[2] = false;
300 :
301 : /*
302 : * Split up the item into chunks
303 : */
304 72238 : while (data_todo > 0)
305 : {
306 : int i;
307 :
308 56902 : CHECK_FOR_INTERRUPTS();
309 :
310 : /*
311 : * Calculate the size of this chunk
312 : */
313 56902 : chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
314 :
315 : /*
316 : * Build a tuple and store it
317 : */
318 56902 : t_values[1] = Int32GetDatum(chunk_seq++);
319 56902 : SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
320 56902 : memcpy(VARDATA(&chunk_data), data_p, chunk_size);
321 56902 : toasttup = heap_form_tuple(toasttupDesc, t_values, t_isnull);
322 :
323 56902 : heap_insert(toastrel, toasttup, mycid, options, NULL);
324 :
325 : /*
326 : * Create the index entry. We cheat a little here by not using
327 : * FormIndexDatum: this relies on the knowledge that the index columns
328 : * are the same as the initial columns of the table for all the
329 : * indexes. We also cheat by not providing an IndexInfo: this is okay
330 : * for now because btree doesn't need one, but we might have to be
331 : * more honest someday.
332 : *
333 : * Note also that there had better not be any user-created index on
334 : * the TOAST table, since we don't bother to update anything else.
335 : */
336 113804 : for (i = 0; i < num_indexes; i++)
337 : {
338 : /* Only index relations marked as ready can be updated */
339 56902 : if (toastidxs[i]->rd_index->indisready)
340 56902 : index_insert(toastidxs[i], t_values, t_isnull,
341 : &(toasttup->t_self),
342 : toastrel,
343 56902 : toastidxs[i]->rd_index->indisunique ?
344 : UNIQUE_CHECK_YES : UNIQUE_CHECK_NO,
345 : false, NULL);
346 : }
347 :
348 : /*
349 : * Free memory
350 : */
351 56902 : heap_freetuple(toasttup);
352 :
353 : /*
354 : * Move on to next chunk
355 : */
356 56902 : data_todo -= chunk_size;
357 56902 : data_p += chunk_size;
358 : }
359 :
360 : /*
361 : * Done - close toast relation and its indexes but keep the lock until
362 : * commit, so as a concurrent reindex done directly on the toast relation
363 : * would be able to wait for this transaction.
364 : */
365 15336 : toast_close_indexes(toastidxs, num_indexes, NoLock);
366 15336 : table_close(toastrel, NoLock);
367 :
368 : /*
369 : * Create the TOAST pointer value that we'll return
370 : */
371 15336 : result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
372 15336 : SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
373 15336 : memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
374 :
375 15336 : return PointerGetDatum(result);
376 : }
377 :
378 : /* ----------
379 : * toast_delete_datum -
380 : *
381 : * Delete a single external stored value.
382 : * ----------
383 : */
384 : void
385 1176 : toast_delete_datum(Relation rel, Datum value, bool is_speculative)
386 : {
387 1176 : struct varlena *attr = (struct varlena *) DatumGetPointer(value);
388 : struct varatt_external toast_pointer;
389 : Relation toastrel;
390 : Relation *toastidxs;
391 : ScanKeyData toastkey;
392 : SysScanDesc toastscan;
393 : HeapTuple toasttup;
394 : int num_indexes;
395 : int validIndex;
396 :
397 1176 : if (!VARATT_IS_EXTERNAL_ONDISK(attr))
398 0 : return;
399 :
400 : /* Must copy to access aligned fields */
401 1176 : VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
402 :
403 : /*
404 : * Open the toast relation and its indexes
405 : */
406 1176 : toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
407 :
408 : /* Fetch valid relation used for process */
409 1176 : validIndex = toast_open_indexes(toastrel,
410 : RowExclusiveLock,
411 : &toastidxs,
412 : &num_indexes);
413 :
414 : /*
415 : * Setup a scan key to find chunks with matching va_valueid
416 : */
417 1176 : ScanKeyInit(&toastkey,
418 : (AttrNumber) 1,
419 : BTEqualStrategyNumber, F_OIDEQ,
420 : ObjectIdGetDatum(toast_pointer.va_valueid));
421 :
422 : /*
423 : * Find all the chunks. (We don't actually care whether we see them in
424 : * sequence or not, but since we've already locked the index we might as
425 : * well use systable_beginscan_ordered.)
426 : */
427 1176 : toastscan = systable_beginscan_ordered(toastrel, toastidxs[validIndex],
428 : get_toast_snapshot(), 1, &toastkey);
429 5670 : while ((toasttup = systable_getnext_ordered(toastscan, ForwardScanDirection)) != NULL)
430 : {
431 : /*
432 : * Have a chunk, delete it
433 : */
434 4494 : if (is_speculative)
435 10 : heap_abort_speculative(toastrel, &toasttup->t_self);
436 : else
437 4484 : simple_heap_delete(toastrel, &toasttup->t_self);
438 : }
439 :
440 : /*
441 : * End scan and close relations but keep the lock until commit, so as a
442 : * concurrent reindex done directly on the toast relation would be able to
443 : * wait for this transaction.
444 : */
445 1176 : systable_endscan_ordered(toastscan);
446 1176 : toast_close_indexes(toastidxs, num_indexes, NoLock);
447 1176 : table_close(toastrel, NoLock);
448 : }
449 :
450 : /* ----------
451 : * toastrel_valueid_exists -
452 : *
453 : * Test whether a toast value with the given ID exists in the toast relation.
454 : * For safety, we consider a value to exist if there are either live or dead
455 : * toast rows with that ID; see notes for GetNewOidWithIndex().
456 : * ----------
457 : */
458 : static bool
459 580 : toastrel_valueid_exists(Relation toastrel, Oid valueid)
460 : {
461 580 : bool result = false;
462 : ScanKeyData toastkey;
463 : SysScanDesc toastscan;
464 : int num_indexes;
465 : int validIndex;
466 : Relation *toastidxs;
467 :
468 : /* Fetch a valid index relation */
469 580 : validIndex = toast_open_indexes(toastrel,
470 : RowExclusiveLock,
471 : &toastidxs,
472 : &num_indexes);
473 :
474 : /*
475 : * Setup a scan key to find chunks with matching va_valueid
476 : */
477 580 : ScanKeyInit(&toastkey,
478 : (AttrNumber) 1,
479 : BTEqualStrategyNumber, F_OIDEQ,
480 : ObjectIdGetDatum(valueid));
481 :
482 : /*
483 : * Is there any such chunk?
484 : */
485 580 : toastscan = systable_beginscan(toastrel,
486 580 : RelationGetRelid(toastidxs[validIndex]),
487 : true, SnapshotAny, 1, &toastkey);
488 :
489 580 : if (systable_getnext(toastscan) != NULL)
490 0 : result = true;
491 :
492 580 : systable_endscan(toastscan);
493 :
494 : /* Clean up */
495 580 : toast_close_indexes(toastidxs, num_indexes, RowExclusiveLock);
496 :
497 580 : return result;
498 : }
499 :
500 : /* ----------
501 : * toastid_valueid_exists -
502 : *
503 : * As above, but work from toast rel's OID not an open relation
504 : * ----------
505 : */
506 : static bool
507 0 : toastid_valueid_exists(Oid toastrelid, Oid valueid)
508 : {
509 : bool result;
510 : Relation toastrel;
511 :
512 0 : toastrel = table_open(toastrelid, AccessShareLock);
513 :
514 0 : result = toastrel_valueid_exists(toastrel, valueid);
515 :
516 0 : table_close(toastrel, AccessShareLock);
517 :
518 0 : return result;
519 : }
520 :
521 : /* ----------
522 : * toast_get_valid_index
523 : *
524 : * Get OID of valid index associated to given toast relation. A toast
525 : * relation can have only one valid index at the same time.
526 : */
527 : Oid
528 946 : toast_get_valid_index(Oid toastoid, LOCKMODE lock)
529 : {
530 : int num_indexes;
531 : int validIndex;
532 : Oid validIndexOid;
533 : Relation *toastidxs;
534 : Relation toastrel;
535 :
536 : /* Open the toast relation */
537 946 : toastrel = table_open(toastoid, lock);
538 :
539 : /* Look for the valid index of the toast relation */
540 946 : validIndex = toast_open_indexes(toastrel,
541 : lock,
542 : &toastidxs,
543 : &num_indexes);
544 946 : validIndexOid = RelationGetRelid(toastidxs[validIndex]);
545 :
546 : /* Close the toast relation and all its indexes */
547 946 : toast_close_indexes(toastidxs, num_indexes, NoLock);
548 946 : table_close(toastrel, NoLock);
549 :
550 946 : return validIndexOid;
551 : }
552 :
553 : /* ----------
554 : * toast_open_indexes
555 : *
556 : * Get an array of the indexes associated to the given toast relation
557 : * and return as well the position of the valid index used by the toast
558 : * relation in this array. It is the responsibility of the caller of this
559 : * function to close the indexes as well as free them.
560 : */
561 : int
562 39570 : toast_open_indexes(Relation toastrel,
563 : LOCKMODE lock,
564 : Relation **toastidxs,
565 : int *num_indexes)
566 : {
567 39570 : int i = 0;
568 39570 : int res = 0;
569 39570 : bool found = false;
570 : List *indexlist;
571 : ListCell *lc;
572 :
573 : /* Get index list of the toast relation */
574 39570 : indexlist = RelationGetIndexList(toastrel);
575 : Assert(indexlist != NIL);
576 :
577 39570 : *num_indexes = list_length(indexlist);
578 :
579 : /* Open all the index relations */
580 39570 : *toastidxs = (Relation *) palloc(*num_indexes * sizeof(Relation));
581 79140 : foreach(lc, indexlist)
582 39570 : (*toastidxs)[i++] = index_open(lfirst_oid(lc), lock);
583 :
584 : /* Fetch the first valid index in list */
585 39570 : for (i = 0; i < *num_indexes; i++)
586 : {
587 39570 : Relation toastidx = (*toastidxs)[i];
588 :
589 39570 : if (toastidx->rd_index->indisvalid)
590 : {
591 39570 : res = i;
592 39570 : found = true;
593 39570 : break;
594 : }
595 : }
596 :
597 : /*
598 : * Free index list, not necessary anymore as relations are opened and a
599 : * valid index has been found.
600 : */
601 39570 : list_free(indexlist);
602 :
603 : /*
604 : * The toast relation should have one valid index, so something is going
605 : * wrong if there is nothing.
606 : */
607 39570 : if (!found)
608 0 : elog(ERROR, "no valid index found for toast relation with Oid %u",
609 : RelationGetRelid(toastrel));
610 :
611 39570 : return res;
612 : }
613 :
614 : /* ----------
615 : * toast_close_indexes
616 : *
617 : * Close an array of indexes for a toast relation and free it. This should
618 : * be called for a set of indexes opened previously with toast_open_indexes.
619 : */
620 : void
621 39564 : toast_close_indexes(Relation *toastidxs, int num_indexes, LOCKMODE lock)
622 : {
623 : int i;
624 :
625 : /* Close relations and clean up things */
626 79128 : for (i = 0; i < num_indexes; i++)
627 39564 : index_close(toastidxs[i], lock);
628 39564 : pfree(toastidxs);
629 39564 : }
630 :
631 : /* ----------
632 : * get_toast_snapshot
633 : *
634 : * Return the TOAST snapshot. Detoasting *must* happen in the same
635 : * transaction that originally fetched the toast pointer.
636 : */
637 : Snapshot
638 44952 : get_toast_snapshot(void)
639 : {
640 : /*
641 : * We cannot directly check that detoasting happens in the same
642 : * transaction that originally fetched the toast pointer, but at least
643 : * check that the session has some active snapshots. It might not if, for
644 : * example, a procedure fetches a toasted value into a local variable,
645 : * commits, and then tries to detoast the value. Such coding is unsafe,
646 : * because once we commit there is nothing to prevent the toast data from
647 : * being deleted. (This is not very much protection, because in many
648 : * scenarios the procedure would have already created a new transaction
649 : * snapshot, preventing us from detecting the problem. But it's better
650 : * than nothing.)
651 : */
652 44952 : if (!HaveRegisteredOrActiveSnapshot())
653 0 : elog(ERROR, "cannot fetch toast data without an active snapshot");
654 :
655 44952 : return &SnapshotToastData;
656 : }
|