1 /*-------------------------------------------------------------------------
5 * PostgreSQL object comments utility code.
7 * Copyright (c) 1996-2001, PostgreSQL Global Development Group
10 * $Header: /cvsroot/pgsql/src/backend/commands/comment.c,v 1.60 2002/09/04 20:31:14 momjian Exp $
12 *-------------------------------------------------------------------------
17 #include "access/genam.h"
18 #include "access/heapam.h"
19 #include "catalog/catname.h"
20 #include "catalog/indexing.h"
21 #include "catalog/namespace.h"
22 #include "catalog/pg_constraint.h"
23 #include "catalog/pg_description.h"
24 #include "catalog/pg_operator.h"
25 #include "catalog/pg_rewrite.h"
26 #include "catalog/pg_trigger.h"
27 #include "catalog/pg_type.h"
28 #include "commands/comment.h"
29 #include "commands/dbcommands.h"
30 #include "miscadmin.h"
31 #include "parser/parse_func.h"
32 #include "parser/parse_oper.h"
33 #include "parser/parse_type.h"
34 #include "utils/acl.h"
35 #include "utils/builtins.h"
36 #include "utils/fmgroids.h"
37 #include "utils/lsyscache.h"
38 #include "utils/syscache.h"
42 * Static Function Prototypes --
44 * The following protoypes are declared static so as not to conflict
45 * with any other routines outside this module. These routines are
46 * called by the public function CommentObject() routine to create
47 * the appropriate comment for the specific object type.
50 static void CommentRelation(int objtype, List *relname, char *comment);
51 static void CommentAttribute(List *qualname, char *comment);
52 static void CommentDatabase(List *qualname, char *comment);
53 static void CommentNamespace(List *qualname, char *comment);
54 static void CommentRule(List *qualname, char *comment);
55 static void CommentType(List *typename, char *comment);
56 static void CommentAggregate(List *aggregate, List *arguments, char *comment);
57 static void CommentProc(List *function, List *arguments, char *comment);
58 static void CommentOperator(List *opername, List *arguments, char *comment);
59 static void CommentTrigger(List *qualname, char *comment);
60 static void CommentConstraint(List *qualname, char *comment);
66 * This routine is used to add the associated comment into
67 * pg_description for the object specified by the given SQL command.
70 CommentObject(CommentStmt *stmt)
72 switch (stmt->objtype)
74 case COMMENT_ON_INDEX:
75 case COMMENT_ON_SEQUENCE:
76 case COMMENT_ON_TABLE:
78 CommentRelation(stmt->objtype, stmt->objname, stmt->comment);
80 case COMMENT_ON_COLUMN:
81 CommentAttribute(stmt->objname, stmt->comment);
83 case COMMENT_ON_DATABASE:
84 CommentDatabase(stmt->objname, stmt->comment);
87 CommentRule(stmt->objname, stmt->comment);
90 CommentType(stmt->objname, stmt->comment);
92 case COMMENT_ON_AGGREGATE:
93 CommentAggregate(stmt->objname, stmt->objargs, stmt->comment);
95 case COMMENT_ON_FUNCTION:
96 CommentProc(stmt->objname, stmt->objargs, stmt->comment);
98 case COMMENT_ON_OPERATOR:
99 CommentOperator(stmt->objname, stmt->objargs, stmt->comment);
101 case COMMENT_ON_TRIGGER:
102 CommentTrigger(stmt->objname, stmt->comment);
104 case COMMENT_ON_SCHEMA:
105 CommentNamespace(stmt->objname, stmt->comment);
107 case COMMENT_ON_CONSTRAINT:
108 CommentConstraint(stmt->objname, stmt->comment);
111 elog(ERROR, "An attempt was made to comment on a unknown type: %d",
119 * Create a comment for the specified object descriptor. Inserts a new
120 * pg_description tuple, or replaces an existing one with the same key.
122 * If the comment given is null or an empty string, instead delete any
123 * existing comment for the specified key.
126 CreateComments(Oid oid, Oid classoid, int32 subid, char *comment)
128 Relation description;
132 HeapTuple newtuple = NULL;
133 Datum values[Natts_pg_description];
134 char nulls[Natts_pg_description];
135 char replaces[Natts_pg_description];
138 /* Reduce empty-string to NULL case */
139 if (comment != NULL && strlen(comment) == 0)
142 /* Prepare to form or update a tuple, if necessary */
145 for (i = 0; i < Natts_pg_description; i++)
151 values[i++] = ObjectIdGetDatum(oid);
152 values[i++] = ObjectIdGetDatum(classoid);
153 values[i++] = Int32GetDatum(subid);
154 values[i++] = DirectFunctionCall1(textin, CStringGetDatum(comment));
157 /* Use the index to search for a matching old tuple */
159 ScanKeyEntryInitialize(&skey[0],
162 (RegProcedure) F_OIDEQ,
163 ObjectIdGetDatum(oid));
165 ScanKeyEntryInitialize(&skey[1],
168 (RegProcedure) F_OIDEQ,
169 ObjectIdGetDatum(classoid));
171 ScanKeyEntryInitialize(&skey[2],
174 (RegProcedure) F_INT4EQ,
175 Int32GetDatum(subid));
177 description = heap_openr(DescriptionRelationName, RowExclusiveLock);
179 sd = systable_beginscan(description, DescriptionObjIndex, true,
180 SnapshotNow, 3, skey);
182 while ((oldtuple = systable_getnext(sd)) != NULL)
184 /* Found the old tuple, so delete or update it */
187 simple_heap_delete(description, &oldtuple->t_self);
190 newtuple = heap_modifytuple(oldtuple, description, values,
192 simple_heap_update(description, &oldtuple->t_self, newtuple);
195 break; /* Assume there can be only one match */
198 systable_endscan(sd);
200 /* If we didn't find an old tuple, insert a new one */
202 if (newtuple == NULL && comment != NULL)
204 newtuple = heap_formtuple(RelationGetDescr(description),
206 simple_heap_insert(description, newtuple);
209 /* Update indexes, if necessary */
210 if (newtuple != NULL)
212 CatalogUpdateIndexes(description, newtuple);
213 heap_freetuple(newtuple);
218 heap_close(description, NoLock);
222 * DeleteComments -- remove comments for an object
224 * If subid is nonzero then only comments matching it will be removed.
225 * If subid is zero, all comments matching the oid/classoid will be removed
226 * (this corresponds to deleting a whole object).
229 DeleteComments(Oid oid, Oid classoid, int32 subid)
231 Relation description;
237 /* Use the index to search for all matching old tuples */
239 ScanKeyEntryInitialize(&skey[0], 0x0,
240 Anum_pg_description_objoid, F_OIDEQ,
241 ObjectIdGetDatum(oid));
243 ScanKeyEntryInitialize(&skey[1], 0x0,
244 Anum_pg_description_classoid, F_OIDEQ,
245 ObjectIdGetDatum(classoid));
249 ScanKeyEntryInitialize(&skey[2], 0x0,
250 Anum_pg_description_objsubid, F_INT4EQ,
251 Int32GetDatum(subid));
257 description = heap_openr(DescriptionRelationName, RowExclusiveLock);
259 sd = systable_beginscan(description, DescriptionObjIndex, true,
260 SnapshotNow, nkeys, skey);
262 while ((oldtuple = systable_getnext(sd)) != NULL)
263 simple_heap_delete(description, &oldtuple->t_self);
267 systable_endscan(sd);
268 heap_close(description, RowExclusiveLock);
274 * This routine is used to add/drop a comment from a relation, where
275 * a relation is a TABLE, SEQUENCE, VIEW or INDEX. The routine simply
276 * finds the relation name by searching the system cache, locating
277 * the appropriate tuple, and inserting a comment using that
278 * tuple's oid. Its parameters are the relation name and comments.
281 CommentRelation(int objtype, List *relname, char *comment)
286 tgtrel = makeRangeVarFromNameList(relname);
289 * Open the relation. We do this mainly to acquire a lock that
290 * ensures no one else drops the relation before we commit. (If they
291 * did, they'd fail to remove the entry we are about to make in
294 relation = relation_openrv(tgtrel, AccessShareLock);
296 /* Check object security */
297 if (!pg_class_ownercheck(RelationGetRelid(relation), GetUserId()))
298 aclcheck_error(ACLCHECK_NOT_OWNER, RelationGetRelationName(relation));
300 /* Next, verify that the relation type matches the intent */
304 case COMMENT_ON_INDEX:
305 if (relation->rd_rel->relkind != RELKIND_INDEX)
306 elog(ERROR, "relation \"%s\" is not an index",
307 RelationGetRelationName(relation));
309 case COMMENT_ON_SEQUENCE:
310 if (relation->rd_rel->relkind != RELKIND_SEQUENCE)
311 elog(ERROR, "relation \"%s\" is not a sequence",
312 RelationGetRelationName(relation));
314 case COMMENT_ON_TABLE:
315 if (relation->rd_rel->relkind != RELKIND_RELATION)
316 elog(ERROR, "relation \"%s\" is not a table",
317 RelationGetRelationName(relation));
319 case COMMENT_ON_VIEW:
320 if (relation->rd_rel->relkind != RELKIND_VIEW)
321 elog(ERROR, "relation \"%s\" is not a view",
322 RelationGetRelationName(relation));
326 /* Create the comment using the relation's oid */
328 CreateComments(RelationGetRelid(relation), RelOid_pg_class, 0, comment);
330 /* Done, but hold lock until commit */
331 relation_close(relation, NoLock);
335 * CommentAttribute --
337 * This routine is used to add/drop a comment from an attribute
338 * such as a table's column. The routine will check security
339 * restrictions and then attempt to look up the specified
340 * attribute. If successful, a comment is added/dropped, else an
341 * elog() exception is thrown. The parameters are the relation
342 * and attribute names, and the comments
345 CommentAttribute(List *qualname, char *comment)
354 /* Separate relname and attr name */
355 nnames = length(qualname);
357 elog(ERROR, "CommentAttribute: must specify relation.attribute");
358 relname = ltruncate(nnames - 1, listCopy(qualname));
359 attrname = strVal(nth(nnames - 1, qualname));
361 /* Open the containing relation to ensure it won't go away meanwhile */
362 rel = makeRangeVarFromNameList(relname);
363 relation = relation_openrv(rel, AccessShareLock);
365 /* Check object security */
367 if (!pg_class_ownercheck(RelationGetRelid(relation), GetUserId()))
368 aclcheck_error(ACLCHECK_NOT_OWNER, RelationGetRelationName(relation));
370 /* Now, fetch the attribute number from the system cache */
372 attnum = get_attnum(RelationGetRelid(relation), attrname);
373 if (attnum == InvalidAttrNumber)
374 elog(ERROR, "Relation \"%s\" has no column \"%s\"",
375 RelationGetRelationName(relation), attrname);
377 /* Create the comment using the relation's oid */
379 CreateComments(RelationGetRelid(relation), RelOid_pg_class,
380 (int32) attnum, comment);
382 /* Done, but hold lock until commit */
384 relation_close(relation, NoLock);
390 * This routine is used to add/drop any user-comments a user might
391 * have regarding the specified database. The routine will check
392 * security for owner permissions, and, if succesful, will then
393 * attempt to find the oid of the database specified. Once found,
394 * a comment is added/dropped using the CreateComments() routine.
397 CommentDatabase(List *qualname, char *comment)
402 if (length(qualname) != 1)
403 elog(ERROR, "CommentDatabase: database name may not be qualified");
404 database = strVal(lfirst(qualname));
406 /* First get the database OID */
407 oid = get_database_oid(database);
408 if (!OidIsValid(oid))
409 elog(ERROR, "database \"%s\" does not exist", database);
411 /* Allow if the user matches the database dba or is a superuser */
413 if (!(superuser() || is_dbadmin(oid)))
414 elog(ERROR, "you are not permitted to comment on database \"%s\"",
417 /* Only allow comments on the current database */
418 if (oid != MyDatabaseId)
419 elog(ERROR, "Database comments may only be applied to the current database");
421 /* Create the comment with the pg_database oid */
422 CreateComments(oid, RelOid_pg_database, 0, comment);
426 * CommentNamespace --
428 * This routine is used to add/drop any user-comments a user might
429 * have regarding the specified namespace. The routine will check
430 * security for owner permissions, and, if succesful, will then
431 * attempt to find the oid of the namespace specified. Once found,
432 * a comment is added/dropped using the CreateComments() routine.
435 CommentNamespace(List *qualname, char *comment)
441 if (length(qualname) != 1)
442 elog(ERROR, "CommentSchema: schema name may not be qualified");
443 namespace = strVal(lfirst(qualname));
445 oid = GetSysCacheOid(NAMESPACENAME,
446 CStringGetDatum(namespace),
448 if (!OidIsValid(oid))
449 elog(ERROR, "CommentSchema: Schema \"%s\" could not be found",
452 /* Check object security */
453 if (!pg_namespace_ownercheck(oid, GetUserId()))
454 aclcheck_error(ACLCHECK_NOT_OWNER, namespace);
456 /* pg_namespace doesn't have a hard-coded OID, so must look it up */
457 classoid = get_system_catalog_relid(NamespaceRelationName);
459 /* Call CreateComments() to create/drop the comments */
460 CreateComments(oid, classoid, 0, comment);
466 * This routine is used to add/drop any user-comments a user might
467 * have regarding a specified RULE. The rule for commenting is determined by
468 * both its name and the relation to which it refers. The arguments to this
469 * function are the rule name and relation name (merged into a qualified
470 * name), and the comment to add/drop.
472 * Before PG 7.3, rules had unique names across the whole database, and so
473 * the syntax was just COMMENT ON RULE rulename, with no relation name.
474 * For purposes of backwards compatibility, we support that as long as there
475 * is only one rule by the specified name in the database.
478 CommentRule(List *qualname, char *comment)
491 /* Separate relname and trig name */
492 nnames = length(qualname);
495 /* Old-style: only a rule name is given */
496 Relation RewriteRelation;
497 HeapScanDesc scanDesc;
498 ScanKeyData scanKeyData;
500 rulename = strVal(lfirst(qualname));
502 /* Search pg_rewrite for such a rule */
503 ScanKeyEntryInitialize(&scanKeyData,
505 Anum_pg_rewrite_rulename,
507 PointerGetDatum(rulename));
509 RewriteRelation = heap_openr(RewriteRelationName, AccessShareLock);
510 scanDesc = heap_beginscan(RewriteRelation, SnapshotNow,
513 tuple = heap_getnext(scanDesc, ForwardScanDirection);
514 if (HeapTupleIsValid(tuple))
516 reloid = ((Form_pg_rewrite) GETSTRUCT(tuple))->ev_class;
517 ruleoid = HeapTupleGetOid(tuple);
521 elog(ERROR, "rule \"%s\" does not exist", rulename);
522 reloid = ruleoid = 0; /* keep compiler quiet */
525 if (HeapTupleIsValid(tuple = heap_getnext(scanDesc,
526 ForwardScanDirection)))
527 elog(ERROR, "There are multiple rules \"%s\""
528 "\n\tPlease specify a relation name as well as a rule name",
531 heap_endscan(scanDesc);
532 heap_close(RewriteRelation, AccessShareLock);
534 /* Open the owning relation to ensure it won't go away meanwhile */
535 relation = heap_open(reloid, AccessShareLock);
539 /* New-style: rule and relname both provided */
541 relname = ltruncate(nnames - 1, listCopy(qualname));
542 rulename = strVal(nth(nnames - 1, qualname));
544 /* Open the owning relation to ensure it won't go away meanwhile */
545 rel = makeRangeVarFromNameList(relname);
546 relation = heap_openrv(rel, AccessShareLock);
547 reloid = RelationGetRelid(relation);
549 /* Find the rule's pg_rewrite tuple, get its OID */
550 tuple = SearchSysCache(RULERELNAME,
551 ObjectIdGetDatum(reloid),
552 PointerGetDatum(rulename),
554 if (!HeapTupleIsValid(tuple))
555 elog(ERROR, "rule \"%s\" does not exist", rulename);
556 Assert(reloid == ((Form_pg_rewrite) GETSTRUCT(tuple))->ev_class);
557 ruleoid = HeapTupleGetOid(tuple);
558 ReleaseSysCache(tuple);
561 /* Check object security */
563 aclcheck = pg_class_aclcheck(reloid, GetUserId(), ACL_RULE);
564 if (aclcheck != ACLCHECK_OK)
565 aclcheck_error(aclcheck, rulename);
567 /* pg_rewrite doesn't have a hard-coded OID, so must look it up */
568 classoid = get_system_catalog_relid(RewriteRelationName);
570 /* Call CreateComments() to create/drop the comments */
572 CreateComments(ruleoid, classoid, 0, comment);
578 * This routine is used to add/drop any user-comments a user might
579 * have regarding a TYPE. The type is specified by name
580 * and, if found, and the user has appropriate permissions, a
581 * comment will be added/dropped using the CreateComments() routine.
582 * The type's name and the comments are the paramters to this routine.
585 CommentType(List *typename, char *comment)
590 /* XXX a bit of a crock; should accept TypeName in COMMENT syntax */
591 tname = makeNode(TypeName);
592 tname->names = typename;
595 /* Find the type's oid */
597 oid = typenameTypeId(tname);
599 /* Check object security */
601 if (!pg_type_ownercheck(oid, GetUserId()))
602 aclcheck_error(ACLCHECK_NOT_OWNER, TypeNameToString(tname));
604 /* Call CreateComments() to create/drop the comments */
606 CreateComments(oid, RelOid_pg_type, 0, comment);
610 * CommentAggregate --
612 * This routine is used to allow a user to provide comments on an
613 * aggregate function. The aggregate function is determined by both
614 * its name and its argument type, which, with the comments are
615 * the three parameters handed to this routine.
618 CommentAggregate(List *aggregate, List *arguments, char *comment)
620 TypeName *aggtype = (TypeName *) lfirst(arguments);
624 /* First, attempt to determine the base aggregate oid */
626 baseoid = typenameTypeId(aggtype);
630 /* Now, attempt to find the actual tuple in pg_proc */
632 oid = find_aggregate_func("CommentAggregate", aggregate, baseoid);
634 /* Next, validate the user's attempt to comment */
636 if (!pg_proc_ownercheck(oid, GetUserId()))
637 aclcheck_error(ACLCHECK_NOT_OWNER, NameListToString(aggregate));
639 /* Call CreateComments() to create/drop the comments */
641 CreateComments(oid, RelOid_pg_proc, 0, comment);
647 * This routine is used to allow a user to provide comments on an
648 * procedure (function). The procedure is determined by both
649 * its name and its argument list. The argument list is expected to
650 * be a series of parsed nodes pointed to by a List object. If the
651 * comments string is empty, the associated comment is dropped.
654 CommentProc(List *function, List *arguments, char *comment)
658 /* Look up the procedure */
660 oid = LookupFuncNameTypeNames(function, arguments,
663 /* Now, validate the user's ability to comment on this function */
665 if (!pg_proc_ownercheck(oid, GetUserId()))
666 aclcheck_error(ACLCHECK_NOT_OWNER, NameListToString(function));
668 /* Call CreateComments() to create/drop the comments */
670 CreateComments(oid, RelOid_pg_proc, 0, comment);
676 * This routine is used to allow a user to provide comments on an
677 * operator. The operator for commenting is determined by both
678 * its name and its argument list which defines the left and right
679 * hand types the operator will operate on. The argument list is
680 * expected to be a couple of parse nodes pointed to be a List
684 CommentOperator(List *opername, List *arguments, char *comment)
686 TypeName *typenode1 = (TypeName *) lfirst(arguments);
687 TypeName *typenode2 = (TypeName *) lsecond(arguments);
691 /* Look up the operator */
692 oid = LookupOperNameTypeNames(opername, typenode1, typenode2,
695 /* Valid user's ability to comment on this operator */
696 if (!pg_oper_ownercheck(oid, GetUserId()))
697 aclcheck_error(ACLCHECK_NOT_OWNER, NameListToString(opername));
699 /* pg_operator doesn't have a hard-coded OID, so must look it up */
700 classoid = get_system_catalog_relid(OperatorRelationName);
702 /* Call CreateComments() to create/drop the comments */
703 CreateComments(oid, classoid, 0, comment);
709 * This routine is used to allow a user to provide comments on a
710 * trigger event. The trigger for commenting is determined by both
711 * its name and the relation to which it refers. The arguments to this
712 * function are the trigger name and relation name (merged into a qualified
713 * name), and the comment to add/drop.
716 CommentTrigger(List *qualname, char *comment)
724 HeapTuple triggertuple;
726 ScanKeyData entry[2];
729 /* Separate relname and trig name */
730 nnames = length(qualname);
732 elog(ERROR, "CommentTrigger: must specify relation and trigger");
733 relname = ltruncate(nnames - 1, listCopy(qualname));
734 trigname = strVal(nth(nnames - 1, qualname));
736 /* Open the owning relation to ensure it won't go away meanwhile */
737 rel = makeRangeVarFromNameList(relname);
738 relation = heap_openrv(rel, AccessShareLock);
740 /* Check object security */
742 if (!pg_class_ownercheck(RelationGetRelid(relation), GetUserId()))
743 aclcheck_error(ACLCHECK_NOT_OWNER, RelationGetRelationName(relation));
746 * Fetch the trigger tuple from pg_trigger. There can be only one
747 * because of the unique index.
749 pg_trigger = heap_openr(TriggerRelationName, AccessShareLock);
750 ScanKeyEntryInitialize(&entry[0], 0x0,
751 Anum_pg_trigger_tgrelid,
753 ObjectIdGetDatum(RelationGetRelid(relation)));
754 ScanKeyEntryInitialize(&entry[1], 0x0,
755 Anum_pg_trigger_tgname,
757 CStringGetDatum(trigname));
758 scan = systable_beginscan(pg_trigger, TriggerRelidNameIndex, true,
759 SnapshotNow, 2, entry);
760 triggertuple = systable_getnext(scan);
762 /* If no trigger exists for the relation specified, notify user */
764 if (!HeapTupleIsValid(triggertuple))
765 elog(ERROR, "trigger \"%s\" for relation \"%s\" does not exist",
766 trigname, RelationGetRelationName(relation));
768 oid = HeapTupleGetOid(triggertuple);
770 systable_endscan(scan);
772 /* Create the comment with the pg_trigger oid */
774 CreateComments(oid, RelationGetRelid(pg_trigger), 0, comment);
776 /* Done, but hold lock on relation */
778 heap_close(pg_trigger, AccessShareLock);
779 heap_close(relation, NoLock);
784 * CommentConstraint --
786 * Enable commenting on constraints held within the pg_constraint
787 * table. A qualified name is required as constraint names are
788 * unique per relation.
791 CommentConstraint(List *qualname, char *comment)
797 Relation pg_constraint,
802 Oid conOid = InvalidOid;
804 /* Separate relname and constraint name */
805 nnames = length(qualname);
807 elog(ERROR, "CommentConstraint: must specify relation and constraint");
808 relName = ltruncate(nnames - 1, listCopy(qualname));
809 conName = strVal(nth(nnames - 1, qualname));
811 /* Open the owning relation to ensure it won't go away meanwhile */
812 rel = makeRangeVarFromNameList(relName);
813 relation = heap_openrv(rel, AccessShareLock);
815 /* Check object security */
817 if (!pg_class_ownercheck(RelationGetRelid(relation), GetUserId()))
818 aclcheck_error(ACLCHECK_NOT_OWNER, RelationGetRelationName(relation));
821 * Fetch the constraint tuple from pg_constraint. There may be more
822 * than one match, because constraints are not required to have unique
823 * names; if so, error out.
825 pg_constraint = heap_openr(ConstraintRelationName, AccessShareLock);
827 ScanKeyEntryInitialize(&skey[0], 0x0,
828 Anum_pg_constraint_conrelid, F_OIDEQ,
829 ObjectIdGetDatum(RelationGetRelid(relation)));
831 scan = systable_beginscan(pg_constraint, ConstraintRelidIndex, true,
832 SnapshotNow, 1, skey);
834 while (HeapTupleIsValid(tuple = systable_getnext(scan)))
836 Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple);
838 if (strcmp(NameStr(con->conname), conName) == 0)
840 if (OidIsValid(conOid))
841 elog(ERROR, "Relation \"%s\" has multiple constraints named \"%s\"",
842 RelationGetRelationName(relation), conName);
843 conOid = HeapTupleGetOid(tuple);
847 systable_endscan(scan);
849 /* If no constraint exists for the relation specified, notify user */
850 if (!OidIsValid(conOid))
851 elog(ERROR, "constraint \"%s\" for relation \"%s\" does not exist",
852 conName, RelationGetRelationName(relation));
854 /* Create the comment with the pg_constraint oid */
855 CreateComments(conOid, RelationGetRelid(pg_constraint), 0, comment);
857 /* Done, but hold lock on relation */
858 heap_close(pg_constraint, AccessShareLock);
859 heap_close(relation, NoLock);