Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * cluster.c
4 : * CLUSTER a table on an index. This is now also used for VACUUM FULL.
5 : *
6 : * There is hardly anything left of Paul Brown's original implementation...
7 : *
8 : *
9 : * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
10 : * Portions Copyright (c) 1994-5, Regents of the University of California
11 : *
12 : *
13 : * IDENTIFICATION
14 : * src/backend/commands/cluster.c
15 : *
16 : *-------------------------------------------------------------------------
17 : */
18 : #include "postgres.h"
19 :
20 : #include "access/amapi.h"
21 : #include "access/heapam.h"
22 : #include "access/multixact.h"
23 : #include "access/relscan.h"
24 : #include "access/tableam.h"
25 : #include "access/toast_internals.h"
26 : #include "access/transam.h"
27 : #include "access/xact.h"
28 : #include "catalog/catalog.h"
29 : #include "catalog/dependency.h"
30 : #include "catalog/heap.h"
31 : #include "catalog/index.h"
32 : #include "catalog/namespace.h"
33 : #include "catalog/objectaccess.h"
34 : #include "catalog/pg_am.h"
35 : #include "catalog/pg_inherits.h"
36 : #include "catalog/toasting.h"
37 : #include "commands/cluster.h"
38 : #include "commands/defrem.h"
39 : #include "commands/progress.h"
40 : #include "commands/tablecmds.h"
41 : #include "commands/vacuum.h"
42 : #include "miscadmin.h"
43 : #include "optimizer/optimizer.h"
44 : #include "pgstat.h"
45 : #include "storage/bufmgr.h"
46 : #include "storage/lmgr.h"
47 : #include "storage/predicate.h"
48 : #include "utils/acl.h"
49 : #include "utils/fmgroids.h"
50 : #include "utils/guc.h"
51 : #include "utils/inval.h"
52 : #include "utils/lsyscache.h"
53 : #include "utils/memutils.h"
54 : #include "utils/pg_rusage.h"
55 : #include "utils/relmapper.h"
56 : #include "utils/snapmgr.h"
57 : #include "utils/syscache.h"
58 :
59 : /*
60 : * This struct is used to pass around the information on tables to be
61 : * clustered. We need this so we can make a list of them when invoked without
62 : * a specific table/index pair.
63 : */
64 : typedef struct
65 : {
66 : Oid tableOid;
67 : Oid indexOid;
68 : } RelToCluster;
69 :
70 :
71 : static void cluster_multiple_rels(List *rtcs, ClusterParams *params);
72 : static void rebuild_relation(Relation OldHeap, Oid indexOid, bool verbose);
73 : static void copy_table_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex,
74 : bool verbose, bool *pSwapToastByContent,
75 : TransactionId *pFreezeXid, MultiXactId *pCutoffMulti);
76 : static List *get_tables_to_cluster(MemoryContext cluster_context);
77 : static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context,
78 : Oid indexOid);
79 : static bool cluster_is_permitted_for_relation(Oid relid, Oid userid);
80 :
81 :
82 : /*---------------------------------------------------------------------------
83 : * This cluster code allows for clustering multiple tables at once. Because
84 : * of this, we cannot just run everything on a single transaction, or we
85 : * would be forced to acquire exclusive locks on all the tables being
86 : * clustered, simultaneously --- very likely leading to deadlock.
87 : *
88 : * To solve this we follow a similar strategy to VACUUM code,
89 : * clustering each relation in a separate transaction. For this to work,
90 : * we need to:
91 : * - provide a separate memory context so that we can pass information in
92 : * a way that survives across transactions
93 : * - start a new transaction every time a new relation is clustered
94 : * - check for validity of the information on to-be-clustered relations,
95 : * as someone might have deleted a relation behind our back, or
96 : * clustered one on a different index
97 : * - end the transaction
98 : *
99 : * The single-relation case does not have any such overhead.
100 : *
101 : * We also allow a relation to be specified without index. In that case,
102 : * the indisclustered bit will be looked up, and an ERROR will be thrown
103 : * if there is no index with the bit set.
104 : *---------------------------------------------------------------------------
105 : */
106 : void
107 236 : cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel)
108 : {
109 : ListCell *lc;
110 236 : ClusterParams params = {0};
111 236 : bool verbose = false;
112 236 : Relation rel = NULL;
113 236 : Oid indexOid = InvalidOid;
114 : MemoryContext cluster_context;
115 : List *rtcs;
116 :
117 : /* Parse option list */
118 248 : foreach(lc, stmt->params)
119 : {
120 12 : DefElem *opt = (DefElem *) lfirst(lc);
121 :
122 12 : if (strcmp(opt->defname, "verbose") == 0)
123 12 : verbose = defGetBoolean(opt);
124 : else
125 0 : ereport(ERROR,
126 : (errcode(ERRCODE_SYNTAX_ERROR),
127 : errmsg("unrecognized CLUSTER option \"%s\"",
128 : opt->defname),
129 : parser_errposition(pstate, opt->location)));
130 : }
131 :
132 236 : params.options = (verbose ? CLUOPT_VERBOSE : 0);
133 :
134 236 : if (stmt->relation != NULL)
135 : {
136 : /* This is the single-relation case. */
137 : Oid tableOid;
138 :
139 : /*
140 : * Find, lock, and check permissions on the table. We obtain
141 : * AccessExclusiveLock right away to avoid lock-upgrade hazard in the
142 : * single-transaction case.
143 : */
144 208 : tableOid = RangeVarGetRelidExtended(stmt->relation,
145 : AccessExclusiveLock,
146 : 0,
147 : RangeVarCallbackMaintainsTable,
148 : NULL);
149 196 : rel = table_open(tableOid, NoLock);
150 :
151 : /*
152 : * Reject clustering a remote temp table ... their local buffer
153 : * manager is not going to cope.
154 : */
155 196 : if (RELATION_IS_OTHER_TEMP(rel))
156 0 : ereport(ERROR,
157 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
158 : errmsg("cannot cluster temporary tables of other sessions")));
159 :
160 196 : if (stmt->indexname == NULL)
161 : {
162 : ListCell *index;
163 :
164 : /* We need to find the index that has indisclustered set. */
165 46 : foreach(index, RelationGetIndexList(rel))
166 : {
167 34 : indexOid = lfirst_oid(index);
168 34 : if (get_index_isclustered(indexOid))
169 22 : break;
170 12 : indexOid = InvalidOid;
171 : }
172 :
173 34 : if (!OidIsValid(indexOid))
174 12 : ereport(ERROR,
175 : (errcode(ERRCODE_UNDEFINED_OBJECT),
176 : errmsg("there is no previously clustered index for table \"%s\"",
177 : stmt->relation->relname)));
178 : }
179 : else
180 : {
181 : /*
182 : * The index is expected to be in the same namespace as the
183 : * relation.
184 : */
185 162 : indexOid = get_relname_relid(stmt->indexname,
186 162 : rel->rd_rel->relnamespace);
187 162 : if (!OidIsValid(indexOid))
188 0 : ereport(ERROR,
189 : (errcode(ERRCODE_UNDEFINED_OBJECT),
190 : errmsg("index \"%s\" for table \"%s\" does not exist",
191 : stmt->indexname, stmt->relation->relname)));
192 : }
193 :
194 184 : if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
195 : {
196 : /* close relation, keep lock till commit */
197 158 : table_close(rel, NoLock);
198 :
199 : /* Do the job. */
200 158 : cluster_rel(tableOid, indexOid, ¶ms);
201 :
202 158 : return;
203 : }
204 : }
205 :
206 : /*
207 : * By here, we know we are in a multi-table situation. In order to avoid
208 : * holding locks for too long, we want to process each table in its own
209 : * transaction. This forces us to disallow running inside a user
210 : * transaction block.
211 : */
212 54 : PreventInTransactionBlock(isTopLevel, "CLUSTER");
213 :
214 : /* Also, we need a memory context to hold our list of relations */
215 54 : cluster_context = AllocSetContextCreate(PortalContext,
216 : "Cluster",
217 : ALLOCSET_DEFAULT_SIZES);
218 :
219 : /*
220 : * Either we're processing a partitioned table, or we were not given any
221 : * table name at all. In either case, obtain a list of relations to
222 : * process.
223 : *
224 : * In the former case, an index name must have been given, so we don't
225 : * need to recheck its "indisclustered" bit, but we have to check that it
226 : * is an index that we can cluster on. In the latter case, we set the
227 : * option bit to have indisclustered verified.
228 : *
229 : * Rechecking the relation itself is necessary here in all cases.
230 : */
231 54 : params.options |= CLUOPT_RECHECK;
232 54 : if (rel != NULL)
233 : {
234 : Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
235 26 : check_index_is_clusterable(rel, indexOid, AccessShareLock);
236 20 : rtcs = get_tables_to_cluster_partitioned(cluster_context, indexOid);
237 :
238 : /* close relation, releasing lock on parent table */
239 20 : table_close(rel, AccessExclusiveLock);
240 : }
241 : else
242 : {
243 28 : rtcs = get_tables_to_cluster(cluster_context);
244 28 : params.options |= CLUOPT_RECHECK_ISCLUSTERED;
245 : }
246 :
247 : /* Do the job. */
248 48 : cluster_multiple_rels(rtcs, ¶ms);
249 :
250 : /* Start a new transaction for the cleanup work. */
251 48 : StartTransactionCommand();
252 :
253 : /* Clean up working storage */
254 48 : MemoryContextDelete(cluster_context);
255 : }
256 :
257 : /*
258 : * Given a list of relations to cluster, process each of them in a separate
259 : * transaction.
260 : *
261 : * We expect to be in a transaction at start, but there isn't one when we
262 : * return.
263 : */
264 : static void
265 48 : cluster_multiple_rels(List *rtcs, ClusterParams *params)
266 : {
267 : ListCell *lc;
268 :
269 : /* Commit to get out of starting transaction */
270 48 : PopActiveSnapshot();
271 48 : CommitTransactionCommand();
272 :
273 : /* Cluster the tables, each in a separate transaction */
274 78 : foreach(lc, rtcs)
275 : {
276 30 : RelToCluster *rtc = (RelToCluster *) lfirst(lc);
277 :
278 : /* Start a new transaction for each relation. */
279 30 : StartTransactionCommand();
280 :
281 : /* functions in indexes may want a snapshot set */
282 30 : PushActiveSnapshot(GetTransactionSnapshot());
283 :
284 : /* Do the job. */
285 30 : cluster_rel(rtc->tableOid, rtc->indexOid, params);
286 :
287 30 : PopActiveSnapshot();
288 30 : CommitTransactionCommand();
289 : }
290 48 : }
291 :
292 : /*
293 : * cluster_rel
294 : *
295 : * This clusters the table by creating a new, clustered table and
296 : * swapping the relfilenumbers of the new table and the old table, so
297 : * the OID of the original table is preserved. Thus we do not lose
298 : * GRANT, inheritance nor references to this table (this was a bug
299 : * in releases through 7.3).
300 : *
301 : * Indexes are rebuilt too, via REINDEX. Since we are effectively bulk-loading
302 : * the new table, it's better to create the indexes afterwards than to fill
303 : * them incrementally while we load the table.
304 : *
305 : * If indexOid is InvalidOid, the table will be rewritten in physical order
306 : * instead of index order. This is the new implementation of VACUUM FULL,
307 : * and error messages should refer to the operation as VACUUM not CLUSTER.
308 : */
309 : void
310 546 : cluster_rel(Oid tableOid, Oid indexOid, ClusterParams *params)
311 : {
312 : Relation OldHeap;
313 : Oid save_userid;
314 : int save_sec_context;
315 : int save_nestlevel;
316 546 : bool verbose = ((params->options & CLUOPT_VERBOSE) != 0);
317 546 : bool recheck = ((params->options & CLUOPT_RECHECK) != 0);
318 :
319 : /* Check for user-requested abort. */
320 546 : CHECK_FOR_INTERRUPTS();
321 :
322 546 : pgstat_progress_start_command(PROGRESS_COMMAND_CLUSTER, tableOid);
323 546 : if (OidIsValid(indexOid))
324 188 : pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND,
325 : PROGRESS_CLUSTER_COMMAND_CLUSTER);
326 : else
327 358 : pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND,
328 : PROGRESS_CLUSTER_COMMAND_VACUUM_FULL);
329 :
330 : /*
331 : * We grab exclusive access to the target rel and index for the duration
332 : * of the transaction. (This is redundant for the single-transaction
333 : * case, since cluster() already did it.) The index lock is taken inside
334 : * check_index_is_clusterable.
335 : */
336 546 : OldHeap = try_relation_open(tableOid, AccessExclusiveLock);
337 :
338 : /* If the table has gone away, we can skip processing it */
339 546 : if (!OldHeap)
340 : {
341 0 : pgstat_progress_end_command();
342 0 : return;
343 : }
344 :
345 : /*
346 : * Switch to the table owner's userid, so that any index functions are run
347 : * as that user. Also lock down security-restricted operations and
348 : * arrange to make GUC variable changes local to this command.
349 : */
350 546 : GetUserIdAndSecContext(&save_userid, &save_sec_context);
351 546 : SetUserIdAndSecContext(OldHeap->rd_rel->relowner,
352 : save_sec_context | SECURITY_RESTRICTED_OPERATION);
353 546 : save_nestlevel = NewGUCNestLevel();
354 546 : RestrictSearchPath();
355 :
356 : /*
357 : * Since we may open a new transaction for each relation, we have to check
358 : * that the relation still is what we think it is.
359 : *
360 : * If this is a single-transaction CLUSTER, we can skip these tests. We
361 : * *must* skip the one on indisclustered since it would reject an attempt
362 : * to cluster a not-previously-clustered index.
363 : */
364 546 : if (recheck)
365 : {
366 : /* Check that the user still has privileges for the relation */
367 30 : if (!cluster_is_permitted_for_relation(tableOid, save_userid))
368 : {
369 0 : relation_close(OldHeap, AccessExclusiveLock);
370 0 : goto out;
371 : }
372 :
373 : /*
374 : * Silently skip a temp table for a remote session. Only doing this
375 : * check in the "recheck" case is appropriate (which currently means
376 : * somebody is executing a database-wide CLUSTER or on a partitioned
377 : * table), because there is another check in cluster() which will stop
378 : * any attempt to cluster remote temp tables by name. There is
379 : * another check in cluster_rel which is redundant, but we leave it
380 : * for extra safety.
381 : */
382 30 : if (RELATION_IS_OTHER_TEMP(OldHeap))
383 : {
384 0 : relation_close(OldHeap, AccessExclusiveLock);
385 0 : goto out;
386 : }
387 :
388 30 : if (OidIsValid(indexOid))
389 : {
390 : /*
391 : * Check that the index still exists
392 : */
393 30 : if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid)))
394 : {
395 0 : relation_close(OldHeap, AccessExclusiveLock);
396 0 : goto out;
397 : }
398 :
399 : /*
400 : * Check that the index is still the one with indisclustered set,
401 : * if needed.
402 : */
403 30 : if ((params->options & CLUOPT_RECHECK_ISCLUSTERED) != 0 &&
404 6 : !get_index_isclustered(indexOid))
405 : {
406 0 : relation_close(OldHeap, AccessExclusiveLock);
407 0 : goto out;
408 : }
409 : }
410 : }
411 :
412 : /*
413 : * We allow VACUUM FULL, but not CLUSTER, on shared catalogs. CLUSTER
414 : * would work in most respects, but the index would only get marked as
415 : * indisclustered in the current database, leading to unexpected behavior
416 : * if CLUSTER were later invoked in another database.
417 : */
418 546 : if (OidIsValid(indexOid) && OldHeap->rd_rel->relisshared)
419 0 : ereport(ERROR,
420 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
421 : errmsg("cannot cluster a shared catalog")));
422 :
423 : /*
424 : * Don't process temp tables of other backends ... their local buffer
425 : * manager is not going to cope.
426 : */
427 546 : if (RELATION_IS_OTHER_TEMP(OldHeap))
428 : {
429 0 : if (OidIsValid(indexOid))
430 0 : ereport(ERROR,
431 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
432 : errmsg("cannot cluster temporary tables of other sessions")));
433 : else
434 0 : ereport(ERROR,
435 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
436 : errmsg("cannot vacuum temporary tables of other sessions")));
437 : }
438 :
439 : /*
440 : * Also check for active uses of the relation in the current transaction,
441 : * including open scans and pending AFTER trigger events.
442 : */
443 546 : CheckTableNotInUse(OldHeap, OidIsValid(indexOid) ? "CLUSTER" : "VACUUM");
444 :
445 : /* Check heap and index are valid to cluster on */
446 546 : if (OidIsValid(indexOid))
447 188 : check_index_is_clusterable(OldHeap, indexOid, AccessExclusiveLock);
448 :
449 : /*
450 : * Quietly ignore the request if this is a materialized view which has not
451 : * been populated from its query. No harm is done because there is no data
452 : * to deal with, and we don't want to throw an error if this is part of a
453 : * multi-relation request -- for example, CLUSTER was run on the entire
454 : * database.
455 : */
456 546 : if (OldHeap->rd_rel->relkind == RELKIND_MATVIEW &&
457 0 : !RelationIsPopulated(OldHeap))
458 : {
459 0 : relation_close(OldHeap, AccessExclusiveLock);
460 0 : goto out;
461 : }
462 :
463 : Assert(OldHeap->rd_rel->relkind == RELKIND_RELATION ||
464 : OldHeap->rd_rel->relkind == RELKIND_MATVIEW ||
465 : OldHeap->rd_rel->relkind == RELKIND_TOASTVALUE);
466 :
467 : /*
468 : * All predicate locks on the tuples or pages are about to be made
469 : * invalid, because we move tuples around. Promote them to relation
470 : * locks. Predicate locks on indexes will be promoted when they are
471 : * reindexed.
472 : */
473 546 : TransferPredicateLocksToHeapRelation(OldHeap);
474 :
475 : /* rebuild_relation does all the dirty work */
476 546 : rebuild_relation(OldHeap, indexOid, verbose);
477 :
478 : /* NB: rebuild_relation does table_close() on OldHeap */
479 :
480 540 : out:
481 : /* Roll back any GUC changes executed by index functions */
482 540 : AtEOXact_GUC(false, save_nestlevel);
483 :
484 : /* Restore userid and security context */
485 540 : SetUserIdAndSecContext(save_userid, save_sec_context);
486 :
487 540 : pgstat_progress_end_command();
488 : }
489 :
490 : /*
491 : * Verify that the specified heap and index are valid to cluster on
492 : *
493 : * Side effect: obtains lock on the index. The caller may
494 : * in some cases already have AccessExclusiveLock on the table, but
495 : * not in all cases so we can't rely on the table-level lock for
496 : * protection here.
497 : */
498 : void
499 278 : check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode)
500 : {
501 : Relation OldIndex;
502 :
503 278 : OldIndex = index_open(indexOid, lockmode);
504 :
505 : /*
506 : * Check that index is in fact an index on the given relation
507 : */
508 278 : if (OldIndex->rd_index == NULL ||
509 278 : OldIndex->rd_index->indrelid != RelationGetRelid(OldHeap))
510 0 : ereport(ERROR,
511 : (errcode(ERRCODE_WRONG_OBJECT_TYPE),
512 : errmsg("\"%s\" is not an index for table \"%s\"",
513 : RelationGetRelationName(OldIndex),
514 : RelationGetRelationName(OldHeap))));
515 :
516 : /* Index AM must allow clustering */
517 278 : if (!OldIndex->rd_indam->amclusterable)
518 0 : ereport(ERROR,
519 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
520 : errmsg("cannot cluster on index \"%s\" because access method does not support clustering",
521 : RelationGetRelationName(OldIndex))));
522 :
523 : /*
524 : * Disallow clustering on incomplete indexes (those that might not index
525 : * every row of the relation). We could relax this by making a separate
526 : * seqscan pass over the table to copy the missing rows, but that seems
527 : * expensive and tedious.
528 : */
529 278 : if (!heap_attisnull(OldIndex->rd_indextuple, Anum_pg_index_indpred, NULL))
530 0 : ereport(ERROR,
531 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
532 : errmsg("cannot cluster on partial index \"%s\"",
533 : RelationGetRelationName(OldIndex))));
534 :
535 : /*
536 : * Disallow if index is left over from a failed CREATE INDEX CONCURRENTLY;
537 : * it might well not contain entries for every heap row, or might not even
538 : * be internally consistent. (But note that we don't check indcheckxmin;
539 : * the worst consequence of following broken HOT chains would be that we
540 : * might put recently-dead tuples out-of-order in the new table, and there
541 : * is little harm in that.)
542 : */
543 278 : if (!OldIndex->rd_index->indisvalid)
544 6 : ereport(ERROR,
545 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
546 : errmsg("cannot cluster on invalid index \"%s\"",
547 : RelationGetRelationName(OldIndex))));
548 :
549 : /* Drop relcache refcnt on OldIndex, but keep lock */
550 272 : index_close(OldIndex, NoLock);
551 272 : }
552 :
553 : /*
554 : * mark_index_clustered: mark the specified index as the one clustered on
555 : *
556 : * With indexOid == InvalidOid, will mark all indexes of rel not-clustered.
557 : */
558 : void
559 270 : mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
560 : {
561 : HeapTuple indexTuple;
562 : Form_pg_index indexForm;
563 : Relation pg_index;
564 : ListCell *index;
565 :
566 : /* Disallow applying to a partitioned table */
567 270 : if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
568 12 : ereport(ERROR,
569 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
570 : errmsg("cannot mark index clustered in partitioned table")));
571 :
572 : /*
573 : * If the index is already marked clustered, no need to do anything.
574 : */
575 258 : if (OidIsValid(indexOid))
576 : {
577 246 : if (get_index_isclustered(indexOid))
578 40 : return;
579 : }
580 :
581 : /*
582 : * Check each index of the relation and set/clear the bit as needed.
583 : */
584 218 : pg_index = table_open(IndexRelationId, RowExclusiveLock);
585 :
586 642 : foreach(index, RelationGetIndexList(rel))
587 : {
588 424 : Oid thisIndexOid = lfirst_oid(index);
589 :
590 424 : indexTuple = SearchSysCacheCopy1(INDEXRELID,
591 : ObjectIdGetDatum(thisIndexOid));
592 424 : if (!HeapTupleIsValid(indexTuple))
593 0 : elog(ERROR, "cache lookup failed for index %u", thisIndexOid);
594 424 : indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
595 :
596 : /*
597 : * Unset the bit if set. We know it's wrong because we checked this
598 : * earlier.
599 : */
600 424 : if (indexForm->indisclustered)
601 : {
602 30 : indexForm->indisclustered = false;
603 30 : CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple);
604 : }
605 394 : else if (thisIndexOid == indexOid)
606 : {
607 : /* this was checked earlier, but let's be real sure */
608 206 : if (!indexForm->indisvalid)
609 0 : elog(ERROR, "cannot cluster on invalid index %u", indexOid);
610 206 : indexForm->indisclustered = true;
611 206 : CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple);
612 : }
613 :
614 424 : InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0,
615 : InvalidOid, is_internal);
616 :
617 424 : heap_freetuple(indexTuple);
618 : }
619 :
620 218 : table_close(pg_index, RowExclusiveLock);
621 : }
622 :
623 : /*
624 : * rebuild_relation: rebuild an existing relation in index or physical order
625 : *
626 : * OldHeap: table to rebuild --- must be opened and exclusive-locked!
627 : * indexOid: index to cluster by, or InvalidOid to rewrite in physical order.
628 : *
629 : * NB: this routine closes OldHeap at the right time; caller should not.
630 : */
631 : static void
632 546 : rebuild_relation(Relation OldHeap, Oid indexOid, bool verbose)
633 : {
634 546 : Oid tableOid = RelationGetRelid(OldHeap);
635 546 : Oid accessMethod = OldHeap->rd_rel->relam;
636 546 : Oid tableSpace = OldHeap->rd_rel->reltablespace;
637 : Oid OIDNewHeap;
638 : char relpersistence;
639 : bool is_system_catalog;
640 : bool swap_toast_by_content;
641 : TransactionId frozenXid;
642 : MultiXactId cutoffMulti;
643 :
644 546 : if (OidIsValid(indexOid))
645 : /* Mark the correct index as clustered */
646 188 : mark_index_clustered(OldHeap, indexOid, true);
647 :
648 : /* Remember info about rel before closing OldHeap */
649 546 : relpersistence = OldHeap->rd_rel->relpersistence;
650 546 : is_system_catalog = IsSystemRelation(OldHeap);
651 :
652 : /* Close relcache entry, but keep lock until transaction commit */
653 546 : table_close(OldHeap, NoLock);
654 :
655 : /* Create the transient table that will receive the re-ordered data */
656 546 : OIDNewHeap = make_new_heap(tableOid, tableSpace,
657 : accessMethod,
658 : relpersistence,
659 : AccessExclusiveLock);
660 :
661 : /* Copy the heap data into the new table in the desired order */
662 546 : copy_table_data(OIDNewHeap, tableOid, indexOid, verbose,
663 : &swap_toast_by_content, &frozenXid, &cutoffMulti);
664 :
665 : /*
666 : * Swap the physical files of the target and transient tables, then
667 : * rebuild the target's indexes and throw away the transient table.
668 : */
669 546 : finish_heap_swap(tableOid, OIDNewHeap, is_system_catalog,
670 : swap_toast_by_content, false, true,
671 : frozenXid, cutoffMulti,
672 : relpersistence);
673 540 : }
674 :
675 :
676 : /*
677 : * Create the transient table that will be filled with new data during
678 : * CLUSTER, ALTER TABLE, and similar operations. The transient table
679 : * duplicates the logical structure of the OldHeap; but will have the
680 : * specified physical storage properties NewTableSpace, NewAccessMethod, and
681 : * relpersistence.
682 : *
683 : * After this, the caller should load the new heap with transferred/modified
684 : * data, then call finish_heap_swap to complete the operation.
685 : */
686 : Oid
687 2036 : make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
688 : char relpersistence, LOCKMODE lockmode)
689 : {
690 : TupleDesc OldHeapDesc;
691 : char NewHeapName[NAMEDATALEN];
692 : Oid OIDNewHeap;
693 : Oid toastid;
694 : Relation OldHeap;
695 : HeapTuple tuple;
696 : Datum reloptions;
697 : bool isNull;
698 : Oid namespaceid;
699 :
700 2036 : OldHeap = table_open(OIDOldHeap, lockmode);
701 2036 : OldHeapDesc = RelationGetDescr(OldHeap);
702 :
703 : /*
704 : * Note that the NewHeap will not receive any of the defaults or
705 : * constraints associated with the OldHeap; we don't need 'em, and there's
706 : * no reason to spend cycles inserting them into the catalogs only to
707 : * delete them.
708 : */
709 :
710 : /*
711 : * But we do want to use reloptions of the old heap for new heap.
712 : */
713 2036 : tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(OIDOldHeap));
714 2036 : if (!HeapTupleIsValid(tuple))
715 0 : elog(ERROR, "cache lookup failed for relation %u", OIDOldHeap);
716 2036 : reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions,
717 : &isNull);
718 2036 : if (isNull)
719 1998 : reloptions = (Datum) 0;
720 :
721 2036 : if (relpersistence == RELPERSISTENCE_TEMP)
722 152 : namespaceid = LookupCreationNamespace("pg_temp");
723 : else
724 1884 : namespaceid = RelationGetNamespace(OldHeap);
725 :
726 : /*
727 : * Create the new heap, using a temporary name in the same namespace as
728 : * the existing table. NOTE: there is some risk of collision with user
729 : * relnames. Working around this seems more trouble than it's worth; in
730 : * particular, we can't create the new heap in a different namespace from
731 : * the old, or we will have problems with the TEMP status of temp tables.
732 : *
733 : * Note: the new heap is not a shared relation, even if we are rebuilding
734 : * a shared rel. However, we do make the new heap mapped if the source is
735 : * mapped. This simplifies swap_relation_files, and is absolutely
736 : * necessary for rebuilding pg_class, for reasons explained there.
737 : */
738 2036 : snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
739 :
740 2036 : OIDNewHeap = heap_create_with_catalog(NewHeapName,
741 : namespaceid,
742 : NewTableSpace,
743 : InvalidOid,
744 : InvalidOid,
745 : InvalidOid,
746 2036 : OldHeap->rd_rel->relowner,
747 : NewAccessMethod,
748 : OldHeapDesc,
749 : NIL,
750 : RELKIND_RELATION,
751 : relpersistence,
752 : false,
753 2036 : RelationIsMapped(OldHeap),
754 : ONCOMMIT_NOOP,
755 : reloptions,
756 : false,
757 : true,
758 : true,
759 : OIDOldHeap,
760 : NULL);
761 : Assert(OIDNewHeap != InvalidOid);
762 :
763 2036 : ReleaseSysCache(tuple);
764 :
765 : /*
766 : * Advance command counter so that the newly-created relation's catalog
767 : * tuples will be visible to table_open.
768 : */
769 2036 : CommandCounterIncrement();
770 :
771 : /*
772 : * If necessary, create a TOAST table for the new relation.
773 : *
774 : * If the relation doesn't have a TOAST table already, we can't need one
775 : * for the new relation. The other way around is possible though: if some
776 : * wide columns have been dropped, NewHeapCreateToastTable can decide that
777 : * no TOAST table is needed for the new table.
778 : *
779 : * Note that NewHeapCreateToastTable ends with CommandCounterIncrement, so
780 : * that the TOAST table will be visible for insertion.
781 : */
782 2036 : toastid = OldHeap->rd_rel->reltoastrelid;
783 2036 : if (OidIsValid(toastid))
784 : {
785 : /* keep the existing toast table's reloptions, if any */
786 836 : tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid));
787 836 : if (!HeapTupleIsValid(tuple))
788 0 : elog(ERROR, "cache lookup failed for relation %u", toastid);
789 836 : reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions,
790 : &isNull);
791 836 : if (isNull)
792 836 : reloptions = (Datum) 0;
793 :
794 836 : NewHeapCreateToastTable(OIDNewHeap, reloptions, lockmode, toastid);
795 :
796 836 : ReleaseSysCache(tuple);
797 : }
798 :
799 2036 : table_close(OldHeap, NoLock);
800 :
801 2036 : return OIDNewHeap;
802 : }
803 :
804 : /*
805 : * Do the physical copying of table data.
806 : *
807 : * There are three output parameters:
808 : * *pSwapToastByContent is set true if toast tables must be swapped by content.
809 : * *pFreezeXid receives the TransactionId used as freeze cutoff point.
810 : * *pCutoffMulti receives the MultiXactId used as a cutoff point.
811 : */
812 : static void
813 546 : copy_table_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, bool verbose,
814 : bool *pSwapToastByContent, TransactionId *pFreezeXid,
815 : MultiXactId *pCutoffMulti)
816 : {
817 : Relation NewHeap,
818 : OldHeap,
819 : OldIndex;
820 : Relation relRelation;
821 : HeapTuple reltup;
822 : Form_pg_class relform;
823 : TupleDesc oldTupDesc PG_USED_FOR_ASSERTS_ONLY;
824 : TupleDesc newTupDesc PG_USED_FOR_ASSERTS_ONLY;
825 : VacuumParams params;
826 : struct VacuumCutoffs cutoffs;
827 : bool use_sort;
828 546 : double num_tuples = 0,
829 546 : tups_vacuumed = 0,
830 546 : tups_recently_dead = 0;
831 : BlockNumber num_pages;
832 546 : int elevel = verbose ? INFO : DEBUG2;
833 : PGRUsage ru0;
834 : char *nspname;
835 :
836 546 : pg_rusage_init(&ru0);
837 :
838 : /*
839 : * Open the relations we need.
840 : */
841 546 : NewHeap = table_open(OIDNewHeap, AccessExclusiveLock);
842 546 : OldHeap = table_open(OIDOldHeap, AccessExclusiveLock);
843 546 : if (OidIsValid(OIDOldIndex))
844 188 : OldIndex = index_open(OIDOldIndex, AccessExclusiveLock);
845 : else
846 358 : OldIndex = NULL;
847 :
848 : /* Store a copy of the namespace name for logging purposes */
849 546 : nspname = get_namespace_name(RelationGetNamespace(OldHeap));
850 :
851 : /*
852 : * Their tuple descriptors should be exactly alike, but here we only need
853 : * assume that they have the same number of columns.
854 : */
855 546 : oldTupDesc = RelationGetDescr(OldHeap);
856 546 : newTupDesc = RelationGetDescr(NewHeap);
857 : Assert(newTupDesc->natts == oldTupDesc->natts);
858 :
859 : /*
860 : * If the OldHeap has a toast table, get lock on the toast table to keep
861 : * it from being vacuumed. This is needed because autovacuum processes
862 : * toast tables independently of their main tables, with no lock on the
863 : * latter. If an autovacuum were to start on the toast table after we
864 : * compute our OldestXmin below, it would use a later OldestXmin, and then
865 : * possibly remove as DEAD toast tuples belonging to main tuples we think
866 : * are only RECENTLY_DEAD. Then we'd fail while trying to copy those
867 : * tuples.
868 : *
869 : * We don't need to open the toast relation here, just lock it. The lock
870 : * will be held till end of transaction.
871 : */
872 546 : if (OldHeap->rd_rel->reltoastrelid)
873 184 : LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock);
874 :
875 : /*
876 : * If both tables have TOAST tables, perform toast swap by content. It is
877 : * possible that the old table has a toast table but the new one doesn't,
878 : * if toastable columns have been dropped. In that case we have to do
879 : * swap by links. This is okay because swap by content is only essential
880 : * for system catalogs, and we don't support schema changes for them.
881 : */
882 546 : if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid)
883 : {
884 184 : *pSwapToastByContent = true;
885 :
886 : /*
887 : * When doing swap by content, any toast pointers written into NewHeap
888 : * must use the old toast table's OID, because that's where the toast
889 : * data will eventually be found. Set this up by setting rd_toastoid.
890 : * This also tells toast_save_datum() to preserve the toast value
891 : * OIDs, which we want so as not to invalidate toast pointers in
892 : * system catalog caches, and to avoid making multiple copies of a
893 : * single toast value.
894 : *
895 : * Note that we must hold NewHeap open until we are done writing data,
896 : * since the relcache will not guarantee to remember this setting once
897 : * the relation is closed. Also, this technique depends on the fact
898 : * that no one will try to read from the NewHeap until after we've
899 : * finished writing it and swapping the rels --- otherwise they could
900 : * follow the toast pointers to the wrong place. (It would actually
901 : * work for values copied over from the old toast table, but not for
902 : * any values that we toast which were previously not toasted.)
903 : */
904 184 : NewHeap->rd_toastoid = OldHeap->rd_rel->reltoastrelid;
905 : }
906 : else
907 362 : *pSwapToastByContent = false;
908 :
909 : /*
910 : * Compute xids used to freeze and weed out dead tuples and multixacts.
911 : * Since we're going to rewrite the whole table anyway, there's no reason
912 : * not to be aggressive about this.
913 : */
914 546 : memset(¶ms, 0, sizeof(VacuumParams));
915 546 : vacuum_get_cutoffs(OldHeap, ¶ms, &cutoffs);
916 :
917 : /*
918 : * FreezeXid will become the table's new relfrozenxid, and that mustn't go
919 : * backwards, so take the max.
920 : */
921 : {
922 546 : TransactionId relfrozenxid = OldHeap->rd_rel->relfrozenxid;
923 :
924 1092 : if (TransactionIdIsValid(relfrozenxid) &&
925 546 : TransactionIdPrecedes(cutoffs.FreezeLimit, relfrozenxid))
926 80 : cutoffs.FreezeLimit = relfrozenxid;
927 : }
928 :
929 : /*
930 : * MultiXactCutoff, similarly, shouldn't go backwards either.
931 : */
932 : {
933 546 : MultiXactId relminmxid = OldHeap->rd_rel->relminmxid;
934 :
935 1092 : if (MultiXactIdIsValid(relminmxid) &&
936 546 : MultiXactIdPrecedes(cutoffs.MultiXactCutoff, relminmxid))
937 0 : cutoffs.MultiXactCutoff = relminmxid;
938 : }
939 :
940 : /*
941 : * Decide whether to use an indexscan or seqscan-and-optional-sort to scan
942 : * the OldHeap. We know how to use a sort to duplicate the ordering of a
943 : * btree index, and will use seqscan-and-sort for that case if the planner
944 : * tells us it's cheaper. Otherwise, always indexscan if an index is
945 : * provided, else plain seqscan.
946 : */
947 546 : if (OldIndex != NULL && OldIndex->rd_rel->relam == BTREE_AM_OID)
948 188 : use_sort = plan_cluster_use_sort(OIDOldHeap, OIDOldIndex);
949 : else
950 358 : use_sort = false;
951 :
952 : /* Log what we're doing */
953 546 : if (OldIndex != NULL && !use_sort)
954 78 : ereport(elevel,
955 : (errmsg("clustering \"%s.%s\" using index scan on \"%s\"",
956 : nspname,
957 : RelationGetRelationName(OldHeap),
958 : RelationGetRelationName(OldIndex))));
959 468 : else if (use_sort)
960 110 : ereport(elevel,
961 : (errmsg("clustering \"%s.%s\" using sequential scan and sort",
962 : nspname,
963 : RelationGetRelationName(OldHeap))));
964 : else
965 358 : ereport(elevel,
966 : (errmsg("vacuuming \"%s.%s\"",
967 : nspname,
968 : RelationGetRelationName(OldHeap))));
969 :
970 : /*
971 : * Hand off the actual copying to AM specific function, the generic code
972 : * cannot know how to deal with visibility across AMs. Note that this
973 : * routine is allowed to set FreezeXid / MultiXactCutoff to different
974 : * values (e.g. because the AM doesn't use freezing).
975 : */
976 546 : table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
977 : cutoffs.OldestXmin, &cutoffs.FreezeLimit,
978 : &cutoffs.MultiXactCutoff,
979 : &num_tuples, &tups_vacuumed,
980 : &tups_recently_dead);
981 :
982 : /* return selected values to caller, get set as relfrozenxid/minmxid */
983 546 : *pFreezeXid = cutoffs.FreezeLimit;
984 546 : *pCutoffMulti = cutoffs.MultiXactCutoff;
985 :
986 : /* Reset rd_toastoid just to be tidy --- it shouldn't be looked at again */
987 546 : NewHeap->rd_toastoid = InvalidOid;
988 :
989 546 : num_pages = RelationGetNumberOfBlocks(NewHeap);
990 :
991 : /* Log what we did */
992 546 : ereport(elevel,
993 : (errmsg("\"%s.%s\": found %.0f removable, %.0f nonremovable row versions in %u pages",
994 : nspname,
995 : RelationGetRelationName(OldHeap),
996 : tups_vacuumed, num_tuples,
997 : RelationGetNumberOfBlocks(OldHeap)),
998 : errdetail("%.0f dead row versions cannot be removed yet.\n"
999 : "%s.",
1000 : tups_recently_dead,
1001 : pg_rusage_show(&ru0))));
1002 :
1003 546 : if (OldIndex != NULL)
1004 188 : index_close(OldIndex, NoLock);
1005 546 : table_close(OldHeap, NoLock);
1006 546 : table_close(NewHeap, NoLock);
1007 :
1008 : /* Update pg_class to reflect the correct values of pages and tuples. */
1009 546 : relRelation = table_open(RelationRelationId, RowExclusiveLock);
1010 :
1011 546 : reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(OIDNewHeap));
1012 546 : if (!HeapTupleIsValid(reltup))
1013 0 : elog(ERROR, "cache lookup failed for relation %u", OIDNewHeap);
1014 546 : relform = (Form_pg_class) GETSTRUCT(reltup);
1015 :
1016 546 : relform->relpages = num_pages;
1017 546 : relform->reltuples = num_tuples;
1018 :
1019 : /* Don't update the stats for pg_class. See swap_relation_files. */
1020 546 : if (OIDOldHeap != RelationRelationId)
1021 508 : CatalogTupleUpdate(relRelation, &reltup->t_self, reltup);
1022 : else
1023 38 : CacheInvalidateRelcacheByTuple(reltup);
1024 :
1025 : /* Clean up. */
1026 546 : heap_freetuple(reltup);
1027 546 : table_close(relRelation, RowExclusiveLock);
1028 :
1029 : /* Make the update visible */
1030 546 : CommandCounterIncrement();
1031 546 : }
1032 :
1033 : /*
1034 : * Swap the physical files of two given relations.
1035 : *
1036 : * We swap the physical identity (reltablespace, relfilenumber) while keeping
1037 : * the same logical identities of the two relations. relpersistence is also
1038 : * swapped, which is critical since it determines where buffers live for each
1039 : * relation.
1040 : *
1041 : * We can swap associated TOAST data in either of two ways: recursively swap
1042 : * the physical content of the toast tables (and their indexes), or swap the
1043 : * TOAST links in the given relations' pg_class entries. The former is needed
1044 : * to manage rewrites of shared catalogs (where we cannot change the pg_class
1045 : * links) while the latter is the only way to handle cases in which a toast
1046 : * table is added or removed altogether.
1047 : *
1048 : * Additionally, the first relation is marked with relfrozenxid set to
1049 : * frozenXid. It seems a bit ugly to have this here, but the caller would
1050 : * have to do it anyway, so having it here saves a heap_update. Note: in
1051 : * the swap-toast-links case, we assume we don't need to change the toast
1052 : * table's relfrozenxid: the new version of the toast table should already
1053 : * have relfrozenxid set to RecentXmin, which is good enough.
1054 : *
1055 : * Lastly, if r2 and its toast table and toast index (if any) are mapped,
1056 : * their OIDs are emitted into mapped_tables[]. This is hacky but beats
1057 : * having to look the information up again later in finish_heap_swap.
1058 : */
1059 : static void
1060 2266 : swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
1061 : bool swap_toast_by_content,
1062 : bool is_internal,
1063 : TransactionId frozenXid,
1064 : MultiXactId cutoffMulti,
1065 : Oid *mapped_tables)
1066 : {
1067 : Relation relRelation;
1068 : HeapTuple reltup1,
1069 : reltup2;
1070 : Form_pg_class relform1,
1071 : relform2;
1072 : RelFileNumber relfilenumber1,
1073 : relfilenumber2;
1074 : RelFileNumber swaptemp;
1075 : char swptmpchr;
1076 : Oid relam1,
1077 : relam2;
1078 :
1079 : /* We need writable copies of both pg_class tuples. */
1080 2266 : relRelation = table_open(RelationRelationId, RowExclusiveLock);
1081 :
1082 2266 : reltup1 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r1));
1083 2266 : if (!HeapTupleIsValid(reltup1))
1084 0 : elog(ERROR, "cache lookup failed for relation %u", r1);
1085 2266 : relform1 = (Form_pg_class) GETSTRUCT(reltup1);
1086 :
1087 2266 : reltup2 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r2));
1088 2266 : if (!HeapTupleIsValid(reltup2))
1089 0 : elog(ERROR, "cache lookup failed for relation %u", r2);
1090 2266 : relform2 = (Form_pg_class) GETSTRUCT(reltup2);
1091 :
1092 2266 : relfilenumber1 = relform1->relfilenode;
1093 2266 : relfilenumber2 = relform2->relfilenode;
1094 2266 : relam1 = relform1->relam;
1095 2266 : relam2 = relform2->relam;
1096 :
1097 2266 : if (RelFileNumberIsValid(relfilenumber1) &&
1098 : RelFileNumberIsValid(relfilenumber2))
1099 : {
1100 : /*
1101 : * Normal non-mapped relations: swap relfilenumbers, reltablespaces,
1102 : * relpersistence
1103 : */
1104 : Assert(!target_is_pg_class);
1105 :
1106 2098 : swaptemp = relform1->relfilenode;
1107 2098 : relform1->relfilenode = relform2->relfilenode;
1108 2098 : relform2->relfilenode = swaptemp;
1109 :
1110 2098 : swaptemp = relform1->reltablespace;
1111 2098 : relform1->reltablespace = relform2->reltablespace;
1112 2098 : relform2->reltablespace = swaptemp;
1113 :
1114 2098 : swaptemp = relform1->relam;
1115 2098 : relform1->relam = relform2->relam;
1116 2098 : relform2->relam = swaptemp;
1117 :
1118 2098 : swptmpchr = relform1->relpersistence;
1119 2098 : relform1->relpersistence = relform2->relpersistence;
1120 2098 : relform2->relpersistence = swptmpchr;
1121 :
1122 : /* Also swap toast links, if we're swapping by links */
1123 2098 : if (!swap_toast_by_content)
1124 : {
1125 1666 : swaptemp = relform1->reltoastrelid;
1126 1666 : relform1->reltoastrelid = relform2->reltoastrelid;
1127 1666 : relform2->reltoastrelid = swaptemp;
1128 : }
1129 : }
1130 : else
1131 : {
1132 : /*
1133 : * Mapped-relation case. Here we have to swap the relation mappings
1134 : * instead of modifying the pg_class columns. Both must be mapped.
1135 : */
1136 168 : if (RelFileNumberIsValid(relfilenumber1) ||
1137 : RelFileNumberIsValid(relfilenumber2))
1138 0 : elog(ERROR, "cannot swap mapped relation \"%s\" with non-mapped relation",
1139 : NameStr(relform1->relname));
1140 :
1141 : /*
1142 : * We can't change the tablespace nor persistence of a mapped rel, and
1143 : * we can't handle toast link swapping for one either, because we must
1144 : * not apply any critical changes to its pg_class row. These cases
1145 : * should be prevented by upstream permissions tests, so these checks
1146 : * are non-user-facing emergency backstop.
1147 : */
1148 168 : if (relform1->reltablespace != relform2->reltablespace)
1149 0 : elog(ERROR, "cannot change tablespace of mapped relation \"%s\"",
1150 : NameStr(relform1->relname));
1151 168 : if (relform1->relpersistence != relform2->relpersistence)
1152 0 : elog(ERROR, "cannot change persistence of mapped relation \"%s\"",
1153 : NameStr(relform1->relname));
1154 168 : if (relform1->relam != relform2->relam)
1155 0 : elog(ERROR, "cannot change access method of mapped relation \"%s\"",
1156 : NameStr(relform1->relname));
1157 168 : if (!swap_toast_by_content &&
1158 48 : (relform1->reltoastrelid || relform2->reltoastrelid))
1159 0 : elog(ERROR, "cannot swap toast by links for mapped relation \"%s\"",
1160 : NameStr(relform1->relname));
1161 :
1162 : /*
1163 : * Fetch the mappings --- shouldn't fail, but be paranoid
1164 : */
1165 168 : relfilenumber1 = RelationMapOidToFilenumber(r1, relform1->relisshared);
1166 168 : if (!RelFileNumberIsValid(relfilenumber1))
1167 0 : elog(ERROR, "could not find relation mapping for relation \"%s\", OID %u",
1168 : NameStr(relform1->relname), r1);
1169 168 : relfilenumber2 = RelationMapOidToFilenumber(r2, relform2->relisshared);
1170 168 : if (!RelFileNumberIsValid(relfilenumber2))
1171 0 : elog(ERROR, "could not find relation mapping for relation \"%s\", OID %u",
1172 : NameStr(relform2->relname), r2);
1173 :
1174 : /*
1175 : * Send replacement mappings to relmapper. Note these won't actually
1176 : * take effect until CommandCounterIncrement.
1177 : */
1178 168 : RelationMapUpdateMap(r1, relfilenumber2, relform1->relisshared, false);
1179 168 : RelationMapUpdateMap(r2, relfilenumber1, relform2->relisshared, false);
1180 :
1181 : /* Pass OIDs of mapped r2 tables back to caller */
1182 168 : *mapped_tables++ = r2;
1183 : }
1184 :
1185 : /*
1186 : * Recognize that rel1's relfilenumber (swapped from rel2) is new in this
1187 : * subtransaction. The rel2 storage (swapped from rel1) may or may not be
1188 : * new.
1189 : */
1190 : {
1191 : Relation rel1,
1192 : rel2;
1193 :
1194 2266 : rel1 = relation_open(r1, NoLock);
1195 2266 : rel2 = relation_open(r2, NoLock);
1196 2266 : rel2->rd_createSubid = rel1->rd_createSubid;
1197 2266 : rel2->rd_newRelfilelocatorSubid = rel1->rd_newRelfilelocatorSubid;
1198 2266 : rel2->rd_firstRelfilelocatorSubid = rel1->rd_firstRelfilelocatorSubid;
1199 2266 : RelationAssumeNewRelfilelocator(rel1);
1200 2266 : relation_close(rel1, NoLock);
1201 2266 : relation_close(rel2, NoLock);
1202 : }
1203 :
1204 : /*
1205 : * In the case of a shared catalog, these next few steps will only affect
1206 : * our own database's pg_class row; but that's okay, because they are all
1207 : * noncritical updates. That's also an important fact for the case of a
1208 : * mapped catalog, because it's possible that we'll commit the map change
1209 : * and then fail to commit the pg_class update.
1210 : */
1211 :
1212 : /* set rel1's frozen Xid and minimum MultiXid */
1213 2266 : if (relform1->relkind != RELKIND_INDEX)
1214 : {
1215 : Assert(!TransactionIdIsValid(frozenXid) ||
1216 : TransactionIdIsNormal(frozenXid));
1217 2082 : relform1->relfrozenxid = frozenXid;
1218 2082 : relform1->relminmxid = cutoffMulti;
1219 : }
1220 :
1221 : /* swap size statistics too, since new rel has freshly-updated stats */
1222 : {
1223 : int32 swap_pages;
1224 : float4 swap_tuples;
1225 : int32 swap_allvisible;
1226 :
1227 2266 : swap_pages = relform1->relpages;
1228 2266 : relform1->relpages = relform2->relpages;
1229 2266 : relform2->relpages = swap_pages;
1230 :
1231 2266 : swap_tuples = relform1->reltuples;
1232 2266 : relform1->reltuples = relform2->reltuples;
1233 2266 : relform2->reltuples = swap_tuples;
1234 :
1235 2266 : swap_allvisible = relform1->relallvisible;
1236 2266 : relform1->relallvisible = relform2->relallvisible;
1237 2266 : relform2->relallvisible = swap_allvisible;
1238 : }
1239 :
1240 : /*
1241 : * Update the tuples in pg_class --- unless the target relation of the
1242 : * swap is pg_class itself. In that case, there is zero point in making
1243 : * changes because we'd be updating the old data that we're about to throw
1244 : * away. Because the real work being done here for a mapped relation is
1245 : * just to change the relation map settings, it's all right to not update
1246 : * the pg_class rows in this case. The most important changes will instead
1247 : * performed later, in finish_heap_swap() itself.
1248 : */
1249 2266 : if (!target_is_pg_class)
1250 : {
1251 : CatalogIndexState indstate;
1252 :
1253 2228 : indstate = CatalogOpenIndexes(relRelation);
1254 2228 : CatalogTupleUpdateWithInfo(relRelation, &reltup1->t_self, reltup1,
1255 : indstate);
1256 2228 : CatalogTupleUpdateWithInfo(relRelation, &reltup2->t_self, reltup2,
1257 : indstate);
1258 2228 : CatalogCloseIndexes(indstate);
1259 : }
1260 : else
1261 : {
1262 : /* no update ... but we do still need relcache inval */
1263 38 : CacheInvalidateRelcacheByTuple(reltup1);
1264 38 : CacheInvalidateRelcacheByTuple(reltup2);
1265 : }
1266 :
1267 : /*
1268 : * Now that pg_class has been updated with its relevant information for
1269 : * the swap, update the dependency of the relations to point to their new
1270 : * table AM, if it has changed.
1271 : */
1272 2266 : if (relam1 != relam2)
1273 : {
1274 36 : if (changeDependencyFor(RelationRelationId,
1275 : r1,
1276 : AccessMethodRelationId,
1277 : relam1,
1278 : relam2) != 1)
1279 0 : elog(ERROR, "could not change access method dependency for relation \"%s.%s\"",
1280 : get_namespace_name(get_rel_namespace(r1)),
1281 : get_rel_name(r1));
1282 36 : if (changeDependencyFor(RelationRelationId,
1283 : r2,
1284 : AccessMethodRelationId,
1285 : relam2,
1286 : relam1) != 1)
1287 0 : elog(ERROR, "could not change access method dependency for relation \"%s.%s\"",
1288 : get_namespace_name(get_rel_namespace(r2)),
1289 : get_rel_name(r2));
1290 : }
1291 :
1292 : /*
1293 : * Post alter hook for modified relations. The change to r2 is always
1294 : * internal, but r1 depends on the invocation context.
1295 : */
1296 2266 : InvokeObjectPostAlterHookArg(RelationRelationId, r1, 0,
1297 : InvalidOid, is_internal);
1298 2266 : InvokeObjectPostAlterHookArg(RelationRelationId, r2, 0,
1299 : InvalidOid, true);
1300 :
1301 : /*
1302 : * If we have toast tables associated with the relations being swapped,
1303 : * deal with them too.
1304 : */
1305 2266 : if (relform1->reltoastrelid || relform2->reltoastrelid)
1306 : {
1307 794 : if (swap_toast_by_content)
1308 : {
1309 184 : if (relform1->reltoastrelid && relform2->reltoastrelid)
1310 : {
1311 : /* Recursively swap the contents of the toast tables */
1312 184 : swap_relation_files(relform1->reltoastrelid,
1313 : relform2->reltoastrelid,
1314 : target_is_pg_class,
1315 : swap_toast_by_content,
1316 : is_internal,
1317 : frozenXid,
1318 : cutoffMulti,
1319 : mapped_tables);
1320 : }
1321 : else
1322 : {
1323 : /* caller messed up */
1324 0 : elog(ERROR, "cannot swap toast files by content when there's only one");
1325 : }
1326 : }
1327 : else
1328 : {
1329 : /*
1330 : * We swapped the ownership links, so we need to change dependency
1331 : * data to match.
1332 : *
1333 : * NOTE: it is possible that only one table has a toast table.
1334 : *
1335 : * NOTE: at present, a TOAST table's only dependency is the one on
1336 : * its owning table. If more are ever created, we'd need to use
1337 : * something more selective than deleteDependencyRecordsFor() to
1338 : * get rid of just the link we want.
1339 : */
1340 : ObjectAddress baseobject,
1341 : toastobject;
1342 : long count;
1343 :
1344 : /*
1345 : * We disallow this case for system catalogs, to avoid the
1346 : * possibility that the catalog we're rebuilding is one of the
1347 : * ones the dependency changes would change. It's too late to be
1348 : * making any data changes to the target catalog.
1349 : */
1350 610 : if (IsSystemClass(r1, relform1))
1351 0 : elog(ERROR, "cannot swap toast files by links for system catalogs");
1352 :
1353 : /* Delete old dependencies */
1354 610 : if (relform1->reltoastrelid)
1355 : {
1356 578 : count = deleteDependencyRecordsFor(RelationRelationId,
1357 : relform1->reltoastrelid,
1358 : false);
1359 578 : if (count != 1)
1360 0 : elog(ERROR, "expected one dependency record for TOAST table, found %ld",
1361 : count);
1362 : }
1363 610 : if (relform2->reltoastrelid)
1364 : {
1365 610 : count = deleteDependencyRecordsFor(RelationRelationId,
1366 : relform2->reltoastrelid,
1367 : false);
1368 610 : if (count != 1)
1369 0 : elog(ERROR, "expected one dependency record for TOAST table, found %ld",
1370 : count);
1371 : }
1372 :
1373 : /* Register new dependencies */
1374 610 : baseobject.classId = RelationRelationId;
1375 610 : baseobject.objectSubId = 0;
1376 610 : toastobject.classId = RelationRelationId;
1377 610 : toastobject.objectSubId = 0;
1378 :
1379 610 : if (relform1->reltoastrelid)
1380 : {
1381 578 : baseobject.objectId = r1;
1382 578 : toastobject.objectId = relform1->reltoastrelid;
1383 578 : recordDependencyOn(&toastobject, &baseobject,
1384 : DEPENDENCY_INTERNAL);
1385 : }
1386 :
1387 610 : if (relform2->reltoastrelid)
1388 : {
1389 610 : baseobject.objectId = r2;
1390 610 : toastobject.objectId = relform2->reltoastrelid;
1391 610 : recordDependencyOn(&toastobject, &baseobject,
1392 : DEPENDENCY_INTERNAL);
1393 : }
1394 : }
1395 : }
1396 :
1397 : /*
1398 : * If we're swapping two toast tables by content, do the same for their
1399 : * valid index. The swap can actually be safely done only if the relations
1400 : * have indexes.
1401 : */
1402 2266 : if (swap_toast_by_content &&
1403 552 : relform1->relkind == RELKIND_TOASTVALUE &&
1404 184 : relform2->relkind == RELKIND_TOASTVALUE)
1405 : {
1406 : Oid toastIndex1,
1407 : toastIndex2;
1408 :
1409 : /* Get valid index for each relation */
1410 184 : toastIndex1 = toast_get_valid_index(r1,
1411 : AccessExclusiveLock);
1412 184 : toastIndex2 = toast_get_valid_index(r2,
1413 : AccessExclusiveLock);
1414 :
1415 184 : swap_relation_files(toastIndex1,
1416 : toastIndex2,
1417 : target_is_pg_class,
1418 : swap_toast_by_content,
1419 : is_internal,
1420 : InvalidTransactionId,
1421 : InvalidMultiXactId,
1422 : mapped_tables);
1423 : }
1424 :
1425 : /* Clean up. */
1426 2266 : heap_freetuple(reltup1);
1427 2266 : heap_freetuple(reltup2);
1428 :
1429 2266 : table_close(relRelation, RowExclusiveLock);
1430 2266 : }
1431 :
1432 : /*
1433 : * Remove the transient table that was built by make_new_heap, and finish
1434 : * cleaning up (including rebuilding all indexes on the old heap).
1435 : */
1436 : void
1437 1898 : finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
1438 : bool is_system_catalog,
1439 : bool swap_toast_by_content,
1440 : bool check_constraints,
1441 : bool is_internal,
1442 : TransactionId frozenXid,
1443 : MultiXactId cutoffMulti,
1444 : char newrelpersistence)
1445 : {
1446 : ObjectAddress object;
1447 : Oid mapped_tables[4];
1448 : int reindex_flags;
1449 1898 : ReindexParams reindex_params = {0};
1450 : int i;
1451 :
1452 : /* Report that we are now swapping relation files */
1453 1898 : pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
1454 : PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES);
1455 :
1456 : /* Zero out possible results from swapped_relation_files */
1457 1898 : memset(mapped_tables, 0, sizeof(mapped_tables));
1458 :
1459 : /*
1460 : * Swap the contents of the heap relations (including any toast tables).
1461 : * Also set old heap's relfrozenxid to frozenXid.
1462 : */
1463 1898 : swap_relation_files(OIDOldHeap, OIDNewHeap,
1464 : (OIDOldHeap == RelationRelationId),
1465 : swap_toast_by_content, is_internal,
1466 : frozenXid, cutoffMulti, mapped_tables);
1467 :
1468 : /*
1469 : * If it's a system catalog, queue a sinval message to flush all catcaches
1470 : * on the catalog when we reach CommandCounterIncrement.
1471 : */
1472 1898 : if (is_system_catalog)
1473 216 : CacheInvalidateCatalog(OIDOldHeap);
1474 :
1475 : /*
1476 : * Rebuild each index on the relation (but not the toast table, which is
1477 : * all-new at this point). It is important to do this before the DROP
1478 : * step because if we are processing a system catalog that will be used
1479 : * during DROP, we want to have its indexes available. There is no
1480 : * advantage to the other order anyway because this is all transactional,
1481 : * so no chance to reclaim disk space before commit. We do not need a
1482 : * final CommandCounterIncrement() because reindex_relation does it.
1483 : *
1484 : * Note: because index_build is called via reindex_relation, it will never
1485 : * set indcheckxmin true for the indexes. This is OK even though in some
1486 : * sense we are building new indexes rather than rebuilding existing ones,
1487 : * because the new heap won't contain any HOT chains at all, let alone
1488 : * broken ones, so it can't be necessary to set indcheckxmin.
1489 : */
1490 1898 : reindex_flags = REINDEX_REL_SUPPRESS_INDEX_USE;
1491 1898 : if (check_constraints)
1492 1352 : reindex_flags |= REINDEX_REL_CHECK_CONSTRAINTS;
1493 :
1494 : /*
1495 : * Ensure that the indexes have the same persistence as the parent
1496 : * relation.
1497 : */
1498 1898 : if (newrelpersistence == RELPERSISTENCE_UNLOGGED)
1499 38 : reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED;
1500 1860 : else if (newrelpersistence == RELPERSISTENCE_PERMANENT)
1501 1780 : reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT;
1502 :
1503 : /* Report that we are now reindexing relations */
1504 1898 : pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
1505 : PROGRESS_CLUSTER_PHASE_REBUILD_INDEX);
1506 :
1507 1898 : reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params);
1508 :
1509 : /* Report that we are now doing clean up */
1510 1880 : pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
1511 : PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP);
1512 :
1513 : /*
1514 : * If the relation being rebuilt is pg_class, swap_relation_files()
1515 : * couldn't update pg_class's own pg_class entry (check comments in
1516 : * swap_relation_files()), thus relfrozenxid was not updated. That's
1517 : * annoying because a potential reason for doing a VACUUM FULL is a
1518 : * imminent or actual anti-wraparound shutdown. So, now that we can
1519 : * access the new relation using its indices, update relfrozenxid.
1520 : * pg_class doesn't have a toast relation, so we don't need to update the
1521 : * corresponding toast relation. Not that there's little point moving all
1522 : * relfrozenxid updates here since swap_relation_files() needs to write to
1523 : * pg_class for non-mapped relations anyway.
1524 : */
1525 1880 : if (OIDOldHeap == RelationRelationId)
1526 : {
1527 : Relation relRelation;
1528 : HeapTuple reltup;
1529 : Form_pg_class relform;
1530 :
1531 38 : relRelation = table_open(RelationRelationId, RowExclusiveLock);
1532 :
1533 38 : reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(OIDOldHeap));
1534 38 : if (!HeapTupleIsValid(reltup))
1535 0 : elog(ERROR, "cache lookup failed for relation %u", OIDOldHeap);
1536 38 : relform = (Form_pg_class) GETSTRUCT(reltup);
1537 :
1538 38 : relform->relfrozenxid = frozenXid;
1539 38 : relform->relminmxid = cutoffMulti;
1540 :
1541 38 : CatalogTupleUpdate(relRelation, &reltup->t_self, reltup);
1542 :
1543 38 : table_close(relRelation, RowExclusiveLock);
1544 : }
1545 :
1546 : /* Destroy new heap with old filenumber */
1547 1880 : object.classId = RelationRelationId;
1548 1880 : object.objectId = OIDNewHeap;
1549 1880 : object.objectSubId = 0;
1550 :
1551 : /*
1552 : * The new relation is local to our transaction and we know nothing
1553 : * depends on it, so DROP_RESTRICT should be OK.
1554 : */
1555 1880 : performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
1556 :
1557 : /* performDeletion does CommandCounterIncrement at end */
1558 :
1559 : /*
1560 : * Now we must remove any relation mapping entries that we set up for the
1561 : * transient table, as well as its toast table and toast index if any. If
1562 : * we fail to do this before commit, the relmapper will complain about new
1563 : * permanent map entries being added post-bootstrap.
1564 : */
1565 2048 : for (i = 0; OidIsValid(mapped_tables[i]); i++)
1566 168 : RelationMapRemoveMapping(mapped_tables[i]);
1567 :
1568 : /*
1569 : * At this point, everything is kosher except that, if we did toast swap
1570 : * by links, the toast table's name corresponds to the transient table.
1571 : * The name is irrelevant to the backend because it's referenced by OID,
1572 : * but users looking at the catalogs could be confused. Rename it to
1573 : * prevent this problem.
1574 : *
1575 : * Note no lock required on the relation, because we already hold an
1576 : * exclusive lock on it.
1577 : */
1578 1880 : if (!swap_toast_by_content)
1579 : {
1580 : Relation newrel;
1581 :
1582 1696 : newrel = table_open(OIDOldHeap, NoLock);
1583 1696 : if (OidIsValid(newrel->rd_rel->reltoastrelid))
1584 : {
1585 : Oid toastidx;
1586 : char NewToastName[NAMEDATALEN];
1587 :
1588 : /* Get the associated valid index to be renamed */
1589 578 : toastidx = toast_get_valid_index(newrel->rd_rel->reltoastrelid,
1590 : NoLock);
1591 :
1592 : /* rename the toast table ... */
1593 578 : snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u",
1594 : OIDOldHeap);
1595 578 : RenameRelationInternal(newrel->rd_rel->reltoastrelid,
1596 : NewToastName, true, false);
1597 :
1598 : /* ... and its valid index too. */
1599 578 : snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u_index",
1600 : OIDOldHeap);
1601 :
1602 578 : RenameRelationInternal(toastidx,
1603 : NewToastName, true, true);
1604 :
1605 : /*
1606 : * Reset the relrewrite for the toast. The command-counter
1607 : * increment is required here as we are about to update the tuple
1608 : * that is updated as part of RenameRelationInternal.
1609 : */
1610 578 : CommandCounterIncrement();
1611 578 : ResetRelRewrite(newrel->rd_rel->reltoastrelid);
1612 : }
1613 1696 : relation_close(newrel, NoLock);
1614 : }
1615 :
1616 : /* if it's not a catalog table, clear any missing attribute settings */
1617 1880 : if (!is_system_catalog)
1618 : {
1619 : Relation newrel;
1620 :
1621 1664 : newrel = table_open(OIDOldHeap, NoLock);
1622 1664 : RelationClearMissing(newrel);
1623 1664 : relation_close(newrel, NoLock);
1624 : }
1625 1880 : }
1626 :
1627 :
1628 : /*
1629 : * Get a list of tables that the current user has privileges on and
1630 : * have indisclustered set. Return the list in a List * of RelToCluster
1631 : * (stored in the specified memory context), each one giving the tableOid
1632 : * and the indexOid on which the table is already clustered.
1633 : */
1634 : static List *
1635 28 : get_tables_to_cluster(MemoryContext cluster_context)
1636 : {
1637 : Relation indRelation;
1638 : TableScanDesc scan;
1639 : ScanKeyData entry;
1640 : HeapTuple indexTuple;
1641 : Form_pg_index index;
1642 : MemoryContext old_context;
1643 28 : List *rtcs = NIL;
1644 :
1645 : /*
1646 : * Get all indexes that have indisclustered set and that the current user
1647 : * has the appropriate privileges for.
1648 : */
1649 28 : indRelation = table_open(IndexRelationId, AccessShareLock);
1650 28 : ScanKeyInit(&entry,
1651 : Anum_pg_index_indisclustered,
1652 : BTEqualStrategyNumber, F_BOOLEQ,
1653 : BoolGetDatum(true));
1654 28 : scan = table_beginscan_catalog(indRelation, 1, &entry);
1655 46 : while ((indexTuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
1656 : {
1657 : RelToCluster *rtc;
1658 :
1659 18 : index = (Form_pg_index) GETSTRUCT(indexTuple);
1660 :
1661 18 : if (!cluster_is_permitted_for_relation(index->indrelid, GetUserId()))
1662 12 : continue;
1663 :
1664 : /* Use a permanent memory context for the result list */
1665 6 : old_context = MemoryContextSwitchTo(cluster_context);
1666 :
1667 6 : rtc = (RelToCluster *) palloc(sizeof(RelToCluster));
1668 6 : rtc->tableOid = index->indrelid;
1669 6 : rtc->indexOid = index->indexrelid;
1670 6 : rtcs = lappend(rtcs, rtc);
1671 :
1672 6 : MemoryContextSwitchTo(old_context);
1673 : }
1674 28 : table_endscan(scan);
1675 :
1676 28 : relation_close(indRelation, AccessShareLock);
1677 :
1678 28 : return rtcs;
1679 : }
1680 :
1681 : /*
1682 : * Given an index on a partitioned table, return a list of RelToCluster for
1683 : * all the children leaves tables/indexes.
1684 : *
1685 : * Like expand_vacuum_rel, but here caller must hold AccessExclusiveLock
1686 : * on the table containing the index.
1687 : */
1688 : static List *
1689 20 : get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid indexOid)
1690 : {
1691 : List *inhoids;
1692 : ListCell *lc;
1693 20 : List *rtcs = NIL;
1694 : MemoryContext old_context;
1695 :
1696 : /* Do not lock the children until they're processed */
1697 20 : inhoids = find_all_inheritors(indexOid, NoLock, NULL);
1698 :
1699 104 : foreach(lc, inhoids)
1700 : {
1701 84 : Oid indexrelid = lfirst_oid(lc);
1702 84 : Oid relid = IndexGetRelation(indexrelid, false);
1703 : RelToCluster *rtc;
1704 :
1705 : /* consider only leaf indexes */
1706 84 : if (get_rel_relkind(indexrelid) != RELKIND_INDEX)
1707 38 : continue;
1708 :
1709 : /*
1710 : * It's possible that the user does not have privileges to CLUSTER the
1711 : * leaf partition despite having such privileges on the partitioned
1712 : * table. We skip any partitions which the user is not permitted to
1713 : * CLUSTER.
1714 : */
1715 46 : if (!cluster_is_permitted_for_relation(relid, GetUserId()))
1716 22 : continue;
1717 :
1718 : /* Use a permanent memory context for the result list */
1719 24 : old_context = MemoryContextSwitchTo(cluster_context);
1720 :
1721 24 : rtc = (RelToCluster *) palloc(sizeof(RelToCluster));
1722 24 : rtc->tableOid = relid;
1723 24 : rtc->indexOid = indexrelid;
1724 24 : rtcs = lappend(rtcs, rtc);
1725 :
1726 24 : MemoryContextSwitchTo(old_context);
1727 : }
1728 :
1729 20 : return rtcs;
1730 : }
1731 :
1732 : /*
1733 : * Return whether userid has privileges to CLUSTER relid. If not, this
1734 : * function emits a WARNING.
1735 : */
1736 : static bool
1737 94 : cluster_is_permitted_for_relation(Oid relid, Oid userid)
1738 : {
1739 94 : if (pg_class_aclcheck(relid, userid, ACL_MAINTAIN) == ACLCHECK_OK)
1740 60 : return true;
1741 :
1742 34 : ereport(WARNING,
1743 : (errmsg("permission denied to cluster \"%s\", skipping it",
1744 : get_rel_name(relid))));
1745 34 : return false;
1746 : }
|