Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * comment.c
4 : *
5 : * PostgreSQL object comments utility code.
6 : *
7 : * Copyright (c) 1996-2026, PostgreSQL Global Development Group
8 : *
9 : * IDENTIFICATION
10 : * src/backend/commands/comment.c
11 : *
12 : *-------------------------------------------------------------------------
13 : */
14 :
15 : #include "postgres.h"
16 :
17 : #include "access/genam.h"
18 : #include "access/htup_details.h"
19 : #include "access/relation.h"
20 : #include "access/table.h"
21 : #include "catalog/indexing.h"
22 : #include "catalog/objectaddress.h"
23 : #include "catalog/pg_database.h"
24 : #include "catalog/pg_description.h"
25 : #include "catalog/pg_shdescription.h"
26 : #include "commands/comment.h"
27 : #include "miscadmin.h"
28 : #include "utils/builtins.h"
29 : #include "utils/fmgroids.h"
30 : #include "utils/rel.h"
31 :
32 :
33 : /*
34 : * CommentObject --
35 : *
36 : * This routine is used to add the associated comment into
37 : * pg_description for the object specified by the given SQL command.
38 : */
39 : ObjectAddress
40 4508 : CommentObject(CommentStmt *stmt)
41 : {
42 : Relation relation;
43 4508 : ObjectAddress address = InvalidObjectAddress;
44 : bool missing_ok;
45 :
46 : /*
47 : * When loading a dump, we may see a COMMENT ON DATABASE for the old name
48 : * of the database. Erroring out would prevent pg_restore from completing
49 : * (which is really pg_restore's fault, but for now we will work around
50 : * the problem here). Consensus is that the best fix is to treat wrong
51 : * database name as a WARNING not an ERROR; hence, the following special
52 : * case.
53 : */
54 4508 : if (stmt->objtype == OBJECT_DATABASE)
55 : {
56 138 : char *database = strVal(stmt->object);
57 :
58 138 : if (!OidIsValid(get_database_oid(database, true)))
59 : {
60 0 : ereport(WARNING,
61 : (errcode(ERRCODE_UNDEFINED_DATABASE),
62 : errmsg("database \"%s\" does not exist", database)));
63 0 : return address;
64 : }
65 : }
66 :
67 : /*
68 : * During binary upgrade, allow nonexistent large objects so that we don't
69 : * have to create them during schema restoration. pg_upgrade will
70 : * transfer the contents of pg_largeobject_metadata via COPY or by
71 : * copying/linking its files from the old cluster later on.
72 : */
73 4508 : missing_ok = IsBinaryUpgrade && stmt->objtype == OBJECT_LARGEOBJECT;
74 :
75 : /*
76 : * Translate the parser representation that identifies this object into an
77 : * ObjectAddress. get_object_address() will throw an error if the object
78 : * does not exist, and will also acquire a lock on the target to guard
79 : * against concurrent DROP operations.
80 : */
81 4508 : address = get_object_address(stmt->objtype, stmt->object,
82 : &relation, ShareUpdateExclusiveLock,
83 : missing_ok);
84 :
85 : /* Require ownership of the target object. */
86 4415 : check_object_ownership(GetUserId(), stmt->objtype, address,
87 : stmt->object, relation);
88 :
89 : /* Perform other integrity checks as needed. */
90 4399 : switch (stmt->objtype)
91 : {
92 138 : case OBJECT_COLUMN:
93 :
94 : /*
95 : * Allow comments only on columns of tables, views, materialized
96 : * views, composite types, and foreign tables (which are the only
97 : * relkinds for which pg_dump will dump per-column comments). In
98 : * particular we wish to disallow comments on index columns,
99 : * because the naming of an index's columns may change across PG
100 : * versions, so dumping per-column comments could create reload
101 : * failures.
102 : */
103 138 : if (relation->rd_rel->relkind != RELKIND_RELATION &&
104 37 : relation->rd_rel->relkind != RELKIND_VIEW &&
105 37 : relation->rd_rel->relkind != RELKIND_MATVIEW &&
106 37 : relation->rd_rel->relkind != RELKIND_COMPOSITE_TYPE &&
107 28 : relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
108 12 : relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
109 0 : ereport(ERROR,
110 : (errcode(ERRCODE_WRONG_OBJECT_TYPE),
111 : errmsg("cannot set comment on relation \"%s\"",
112 : RelationGetRelationName(relation)),
113 : errdetail_relkind_not_supported(relation->rd_rel->relkind)));
114 138 : break;
115 4261 : default:
116 4261 : break;
117 : }
118 :
119 : /*
120 : * Databases, tablespaces, and roles are cluster-wide objects, so any
121 : * comments on those objects are recorded in the shared pg_shdescription
122 : * catalog. Comments on all other objects are recorded in pg_description.
123 : */
124 4399 : if (stmt->objtype == OBJECT_DATABASE || stmt->objtype == OBJECT_TABLESPACE
125 4261 : || stmt->objtype == OBJECT_ROLE)
126 155 : CreateSharedComments(address.objectId, address.classId, stmt->comment);
127 : else
128 4244 : CreateComments(address.objectId, address.classId, address.objectSubId,
129 4244 : stmt->comment);
130 :
131 : /*
132 : * If get_object_address() opened the relation for us, we close it to keep
133 : * the reference count correct - but we retain any locks acquired by
134 : * get_object_address() until commit time, to guard against concurrent
135 : * activity.
136 : */
137 4399 : if (relation != NULL)
138 425 : relation_close(relation, NoLock);
139 :
140 4399 : return address;
141 : }
142 :
143 : /*
144 : * CreateComments --
145 : *
146 : * Create a comment for the specified object descriptor. Inserts a new
147 : * pg_description tuple, or replaces an existing one with the same key.
148 : *
149 : * If the comment given is null or an empty string, instead delete any
150 : * existing comment for the specified key.
151 : */
152 : void
153 51550 : CreateComments(Oid oid, Oid classoid, int32 subid, const char *comment)
154 : {
155 : Relation description;
156 : ScanKeyData skey[3];
157 : SysScanDesc sd;
158 : HeapTuple oldtuple;
159 51550 : HeapTuple newtuple = NULL;
160 : Datum values[Natts_pg_description];
161 : bool nulls[Natts_pg_description];
162 : bool replaces[Natts_pg_description];
163 : int i;
164 :
165 : /* Reduce empty-string to NULL case */
166 51550 : if (comment != NULL && strlen(comment) == 0)
167 4 : comment = NULL;
168 :
169 : /* Prepare to form or update a tuple, if necessary */
170 51550 : if (comment != NULL)
171 : {
172 257405 : for (i = 0; i < Natts_pg_description; i++)
173 : {
174 205924 : nulls[i] = false;
175 205924 : replaces[i] = true;
176 : }
177 51481 : values[Anum_pg_description_objoid - 1] = ObjectIdGetDatum(oid);
178 51481 : values[Anum_pg_description_classoid - 1] = ObjectIdGetDatum(classoid);
179 51481 : values[Anum_pg_description_objsubid - 1] = Int32GetDatum(subid);
180 51481 : values[Anum_pg_description_description - 1] = CStringGetTextDatum(comment);
181 : }
182 :
183 : /* Use the index to search for a matching old tuple */
184 :
185 51550 : ScanKeyInit(&skey[0],
186 : Anum_pg_description_objoid,
187 : BTEqualStrategyNumber, F_OIDEQ,
188 : ObjectIdGetDatum(oid));
189 51550 : ScanKeyInit(&skey[1],
190 : Anum_pg_description_classoid,
191 : BTEqualStrategyNumber, F_OIDEQ,
192 : ObjectIdGetDatum(classoid));
193 51550 : ScanKeyInit(&skey[2],
194 : Anum_pg_description_objsubid,
195 : BTEqualStrategyNumber, F_INT4EQ,
196 : Int32GetDatum(subid));
197 :
198 51550 : description = table_open(DescriptionRelationId, RowExclusiveLock);
199 :
200 51550 : sd = systable_beginscan(description, DescriptionObjIndexId, true,
201 : NULL, 3, skey);
202 :
203 51550 : while ((oldtuple = systable_getnext(sd)) != NULL)
204 : {
205 : /* Found the old tuple, so delete or update it */
206 :
207 110 : if (comment == NULL)
208 69 : CatalogTupleDelete(description, &oldtuple->t_self);
209 : else
210 : {
211 41 : newtuple = heap_modify_tuple(oldtuple, RelationGetDescr(description), values,
212 : nulls, replaces);
213 41 : CatalogTupleUpdate(description, &oldtuple->t_self, newtuple);
214 : }
215 :
216 110 : break; /* Assume there can be only one match */
217 : }
218 :
219 51550 : systable_endscan(sd);
220 :
221 : /* If we didn't find an old tuple, insert a new one */
222 :
223 51550 : if (newtuple == NULL && comment != NULL)
224 : {
225 51440 : newtuple = heap_form_tuple(RelationGetDescr(description),
226 : values, nulls);
227 51440 : CatalogTupleInsert(description, newtuple);
228 : }
229 :
230 51550 : if (newtuple != NULL)
231 51481 : heap_freetuple(newtuple);
232 :
233 : /* Done */
234 :
235 51550 : table_close(description, NoLock);
236 51550 : }
237 :
238 : /*
239 : * CreateSharedComments --
240 : *
241 : * Create a comment for the specified shared object descriptor. Inserts a
242 : * new pg_shdescription tuple, or replaces an existing one with the same key.
243 : *
244 : * If the comment given is null or an empty string, instead delete any
245 : * existing comment for the specified key.
246 : */
247 : void
248 155 : CreateSharedComments(Oid oid, Oid classoid, const char *comment)
249 : {
250 : Relation shdescription;
251 : ScanKeyData skey[2];
252 : SysScanDesc sd;
253 : HeapTuple oldtuple;
254 155 : HeapTuple newtuple = NULL;
255 : Datum values[Natts_pg_shdescription];
256 : bool nulls[Natts_pg_shdescription];
257 : bool replaces[Natts_pg_shdescription];
258 : int i;
259 :
260 : /* Reduce empty-string to NULL case */
261 155 : if (comment != NULL && strlen(comment) == 0)
262 4 : comment = NULL;
263 :
264 : /* Prepare to form or update a tuple, if necessary */
265 155 : if (comment != NULL)
266 : {
267 572 : for (i = 0; i < Natts_pg_shdescription; i++)
268 : {
269 429 : nulls[i] = false;
270 429 : replaces[i] = true;
271 : }
272 143 : values[Anum_pg_shdescription_objoid - 1] = ObjectIdGetDatum(oid);
273 143 : values[Anum_pg_shdescription_classoid - 1] = ObjectIdGetDatum(classoid);
274 143 : values[Anum_pg_shdescription_description - 1] = CStringGetTextDatum(comment);
275 : }
276 :
277 : /* Use the index to search for a matching old tuple */
278 :
279 155 : ScanKeyInit(&skey[0],
280 : Anum_pg_shdescription_objoid,
281 : BTEqualStrategyNumber, F_OIDEQ,
282 : ObjectIdGetDatum(oid));
283 155 : ScanKeyInit(&skey[1],
284 : Anum_pg_shdescription_classoid,
285 : BTEqualStrategyNumber, F_OIDEQ,
286 : ObjectIdGetDatum(classoid));
287 :
288 155 : shdescription = table_open(SharedDescriptionRelationId, RowExclusiveLock);
289 :
290 155 : sd = systable_beginscan(shdescription, SharedDescriptionObjIndexId, true,
291 : NULL, 2, skey);
292 :
293 155 : while ((oldtuple = systable_getnext(sd)) != NULL)
294 : {
295 : /* Found the old tuple, so delete or update it */
296 :
297 12 : if (comment == NULL)
298 12 : CatalogTupleDelete(shdescription, &oldtuple->t_self);
299 : else
300 : {
301 0 : newtuple = heap_modify_tuple(oldtuple, RelationGetDescr(shdescription),
302 : values, nulls, replaces);
303 0 : CatalogTupleUpdate(shdescription, &oldtuple->t_self, newtuple);
304 : }
305 :
306 12 : break; /* Assume there can be only one match */
307 : }
308 :
309 155 : systable_endscan(sd);
310 :
311 : /* If we didn't find an old tuple, insert a new one */
312 :
313 155 : if (newtuple == NULL && comment != NULL)
314 : {
315 143 : newtuple = heap_form_tuple(RelationGetDescr(shdescription),
316 : values, nulls);
317 143 : CatalogTupleInsert(shdescription, newtuple);
318 : }
319 :
320 155 : if (newtuple != NULL)
321 143 : heap_freetuple(newtuple);
322 :
323 : /* Done */
324 :
325 155 : table_close(shdescription, NoLock);
326 155 : }
327 :
328 : /*
329 : * DeleteComments -- remove comments for an object
330 : *
331 : * If subid is nonzero then only comments matching it will be removed.
332 : * If subid is zero, all comments matching the oid/classoid will be removed
333 : * (this corresponds to deleting a whole object).
334 : */
335 : void
336 147365 : DeleteComments(Oid oid, Oid classoid, int32 subid)
337 : {
338 : Relation description;
339 : ScanKeyData skey[3];
340 : int nkeys;
341 : SysScanDesc sd;
342 : HeapTuple oldtuple;
343 :
344 : /* Use the index to search for all matching old tuples */
345 :
346 147365 : ScanKeyInit(&skey[0],
347 : Anum_pg_description_objoid,
348 : BTEqualStrategyNumber, F_OIDEQ,
349 : ObjectIdGetDatum(oid));
350 147365 : ScanKeyInit(&skey[1],
351 : Anum_pg_description_classoid,
352 : BTEqualStrategyNumber, F_OIDEQ,
353 : ObjectIdGetDatum(classoid));
354 :
355 147365 : if (subid != 0)
356 : {
357 1404 : ScanKeyInit(&skey[2],
358 : Anum_pg_description_objsubid,
359 : BTEqualStrategyNumber, F_INT4EQ,
360 : Int32GetDatum(subid));
361 1404 : nkeys = 3;
362 : }
363 : else
364 145961 : nkeys = 2;
365 :
366 147365 : description = table_open(DescriptionRelationId, RowExclusiveLock);
367 :
368 147365 : sd = systable_beginscan(description, DescriptionObjIndexId, true,
369 : NULL, nkeys, skey);
370 :
371 147870 : while ((oldtuple = systable_getnext(sd)) != NULL)
372 505 : CatalogTupleDelete(description, &oldtuple->t_self);
373 :
374 : /* Done */
375 :
376 147365 : systable_endscan(sd);
377 147365 : table_close(description, RowExclusiveLock);
378 147365 : }
379 :
380 : /*
381 : * DeleteSharedComments -- remove comments for a shared object
382 : */
383 : void
384 1150 : DeleteSharedComments(Oid oid, Oid classoid)
385 : {
386 : Relation shdescription;
387 : ScanKeyData skey[2];
388 : SysScanDesc sd;
389 : HeapTuple oldtuple;
390 :
391 : /* Use the index to search for all matching old tuples */
392 :
393 1150 : ScanKeyInit(&skey[0],
394 : Anum_pg_shdescription_objoid,
395 : BTEqualStrategyNumber, F_OIDEQ,
396 : ObjectIdGetDatum(oid));
397 1150 : ScanKeyInit(&skey[1],
398 : Anum_pg_shdescription_classoid,
399 : BTEqualStrategyNumber, F_OIDEQ,
400 : ObjectIdGetDatum(classoid));
401 :
402 1150 : shdescription = table_open(SharedDescriptionRelationId, RowExclusiveLock);
403 :
404 1150 : sd = systable_beginscan(shdescription, SharedDescriptionObjIndexId, true,
405 : NULL, 2, skey);
406 :
407 1171 : while ((oldtuple = systable_getnext(sd)) != NULL)
408 21 : CatalogTupleDelete(shdescription, &oldtuple->t_self);
409 :
410 : /* Done */
411 :
412 1150 : systable_endscan(sd);
413 1150 : table_close(shdescription, RowExclusiveLock);
414 1150 : }
415 :
416 : /*
417 : * GetComment -- get the comment for an object, or null if not found.
418 : */
419 : char *
420 1129 : GetComment(Oid oid, Oid classoid, int32 subid)
421 : {
422 : Relation description;
423 : ScanKeyData skey[3];
424 : SysScanDesc sd;
425 : TupleDesc tupdesc;
426 : HeapTuple tuple;
427 : char *comment;
428 :
429 : /* Use the index to search for a matching old tuple */
430 :
431 1129 : ScanKeyInit(&skey[0],
432 : Anum_pg_description_objoid,
433 : BTEqualStrategyNumber, F_OIDEQ,
434 : ObjectIdGetDatum(oid));
435 1129 : ScanKeyInit(&skey[1],
436 : Anum_pg_description_classoid,
437 : BTEqualStrategyNumber, F_OIDEQ,
438 : ObjectIdGetDatum(classoid));
439 1129 : ScanKeyInit(&skey[2],
440 : Anum_pg_description_objsubid,
441 : BTEqualStrategyNumber, F_INT4EQ,
442 : Int32GetDatum(subid));
443 :
444 1129 : description = table_open(DescriptionRelationId, AccessShareLock);
445 1129 : tupdesc = RelationGetDescr(description);
446 :
447 1129 : sd = systable_beginscan(description, DescriptionObjIndexId, true,
448 : NULL, 3, skey);
449 :
450 1129 : comment = NULL;
451 1129 : while ((tuple = systable_getnext(sd)) != NULL)
452 : {
453 : Datum value;
454 : bool isnull;
455 :
456 : /* Found the tuple, get description field */
457 232 : value = heap_getattr(tuple, Anum_pg_description_description, tupdesc, &isnull);
458 232 : if (!isnull)
459 232 : comment = TextDatumGetCString(value);
460 232 : break; /* Assume there can be only one match */
461 : }
462 :
463 1129 : systable_endscan(sd);
464 :
465 : /* Done */
466 1129 : table_close(description, AccessShareLock);
467 :
468 1129 : return comment;
469 : }
|