/src/postgres/src/backend/commands/comment.c
Line | Count | Source |
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 | | CommentObject(CommentStmt *stmt) |
41 | 0 | { |
42 | 0 | Relation relation; |
43 | 0 | ObjectAddress address = InvalidObjectAddress; |
44 | 0 | 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 | 0 | if (stmt->objtype == OBJECT_DATABASE) |
55 | 0 | { |
56 | 0 | char *database = strVal(stmt->object); |
57 | |
|
58 | 0 | if (!OidIsValid(get_database_oid(database, true))) |
59 | 0 | { |
60 | 0 | ereport(WARNING, |
61 | 0 | (errcode(ERRCODE_UNDEFINED_DATABASE), |
62 | 0 | errmsg("database \"%s\" does not exist", database))); |
63 | 0 | return address; |
64 | 0 | } |
65 | 0 | } |
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 | 0 | 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 | 0 | address = get_object_address(stmt->objtype, stmt->object, |
82 | 0 | &relation, ShareUpdateExclusiveLock, |
83 | 0 | missing_ok); |
84 | | |
85 | | /* Require ownership of the target object. */ |
86 | 0 | check_object_ownership(GetUserId(), stmt->objtype, address, |
87 | 0 | stmt->object, relation); |
88 | | |
89 | | /* Perform other integrity checks as needed. */ |
90 | 0 | switch (stmt->objtype) |
91 | 0 | { |
92 | 0 | 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 | 0 | if (relation->rd_rel->relkind != RELKIND_RELATION && |
104 | 0 | relation->rd_rel->relkind != RELKIND_VIEW && |
105 | 0 | relation->rd_rel->relkind != RELKIND_MATVIEW && |
106 | 0 | relation->rd_rel->relkind != RELKIND_COMPOSITE_TYPE && |
107 | 0 | relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE && |
108 | 0 | relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) |
109 | 0 | ereport(ERROR, |
110 | 0 | (errcode(ERRCODE_WRONG_OBJECT_TYPE), |
111 | 0 | errmsg("cannot set comment on relation \"%s\"", |
112 | 0 | RelationGetRelationName(relation)), |
113 | 0 | errdetail_relkind_not_supported(relation->rd_rel->relkind))); |
114 | 0 | break; |
115 | 0 | default: |
116 | 0 | break; |
117 | 0 | } |
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 | 0 | if (stmt->objtype == OBJECT_DATABASE || stmt->objtype == OBJECT_TABLESPACE |
125 | 0 | || stmt->objtype == OBJECT_ROLE) |
126 | 0 | CreateSharedComments(address.objectId, address.classId, stmt->comment); |
127 | 0 | else |
128 | 0 | CreateComments(address.objectId, address.classId, address.objectSubId, |
129 | 0 | 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 | 0 | if (relation != NULL) |
138 | 0 | relation_close(relation, NoLock); |
139 | |
|
140 | 0 | return address; |
141 | 0 | } |
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 | | CreateComments(Oid oid, Oid classoid, int32 subid, const char *comment) |
154 | 0 | { |
155 | 0 | Relation description; |
156 | 0 | ScanKeyData skey[3]; |
157 | 0 | SysScanDesc sd; |
158 | 0 | HeapTuple oldtuple; |
159 | 0 | HeapTuple newtuple = NULL; |
160 | 0 | Datum values[Natts_pg_description]; |
161 | 0 | bool nulls[Natts_pg_description]; |
162 | 0 | bool replaces[Natts_pg_description]; |
163 | 0 | int i; |
164 | | |
165 | | /* Reduce empty-string to NULL case */ |
166 | 0 | if (comment != NULL && strlen(comment) == 0) |
167 | 0 | comment = NULL; |
168 | | |
169 | | /* Prepare to form or update a tuple, if necessary */ |
170 | 0 | if (comment != NULL) |
171 | 0 | { |
172 | 0 | for (i = 0; i < Natts_pg_description; i++) |
173 | 0 | { |
174 | 0 | nulls[i] = false; |
175 | 0 | replaces[i] = true; |
176 | 0 | } |
177 | 0 | values[Anum_pg_description_objoid - 1] = ObjectIdGetDatum(oid); |
178 | 0 | values[Anum_pg_description_classoid - 1] = ObjectIdGetDatum(classoid); |
179 | 0 | values[Anum_pg_description_objsubid - 1] = Int32GetDatum(subid); |
180 | 0 | values[Anum_pg_description_description - 1] = CStringGetTextDatum(comment); |
181 | 0 | } |
182 | | |
183 | | /* Use the index to search for a matching old tuple */ |
184 | |
|
185 | 0 | ScanKeyInit(&skey[0], |
186 | 0 | Anum_pg_description_objoid, |
187 | 0 | BTEqualStrategyNumber, F_OIDEQ, |
188 | 0 | ObjectIdGetDatum(oid)); |
189 | 0 | ScanKeyInit(&skey[1], |
190 | 0 | Anum_pg_description_classoid, |
191 | 0 | BTEqualStrategyNumber, F_OIDEQ, |
192 | 0 | ObjectIdGetDatum(classoid)); |
193 | 0 | ScanKeyInit(&skey[2], |
194 | 0 | Anum_pg_description_objsubid, |
195 | 0 | BTEqualStrategyNumber, F_INT4EQ, |
196 | 0 | Int32GetDatum(subid)); |
197 | |
|
198 | 0 | description = table_open(DescriptionRelationId, RowExclusiveLock); |
199 | |
|
200 | 0 | sd = systable_beginscan(description, DescriptionObjIndexId, true, |
201 | 0 | NULL, 3, skey); |
202 | |
|
203 | 0 | while ((oldtuple = systable_getnext(sd)) != NULL) |
204 | 0 | { |
205 | | /* Found the old tuple, so delete or update it */ |
206 | |
|
207 | 0 | if (comment == NULL) |
208 | 0 | CatalogTupleDelete(description, &oldtuple->t_self); |
209 | 0 | else |
210 | 0 | { |
211 | 0 | newtuple = heap_modify_tuple(oldtuple, RelationGetDescr(description), values, |
212 | 0 | nulls, replaces); |
213 | 0 | CatalogTupleUpdate(description, &oldtuple->t_self, newtuple); |
214 | 0 | } |
215 | |
|
216 | 0 | break; /* Assume there can be only one match */ |
217 | 0 | } |
218 | |
|
219 | 0 | systable_endscan(sd); |
220 | | |
221 | | /* If we didn't find an old tuple, insert a new one */ |
222 | |
|
223 | 0 | if (newtuple == NULL && comment != NULL) |
224 | 0 | { |
225 | 0 | newtuple = heap_form_tuple(RelationGetDescr(description), |
226 | 0 | values, nulls); |
227 | 0 | CatalogTupleInsert(description, newtuple); |
228 | 0 | } |
229 | |
|
230 | 0 | if (newtuple != NULL) |
231 | 0 | heap_freetuple(newtuple); |
232 | | |
233 | | /* Done */ |
234 | |
|
235 | 0 | table_close(description, NoLock); |
236 | 0 | } |
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 | | CreateSharedComments(Oid oid, Oid classoid, const char *comment) |
249 | 0 | { |
250 | 0 | Relation shdescription; |
251 | 0 | ScanKeyData skey[2]; |
252 | 0 | SysScanDesc sd; |
253 | 0 | HeapTuple oldtuple; |
254 | 0 | HeapTuple newtuple = NULL; |
255 | 0 | Datum values[Natts_pg_shdescription]; |
256 | 0 | bool nulls[Natts_pg_shdescription]; |
257 | 0 | bool replaces[Natts_pg_shdescription]; |
258 | 0 | int i; |
259 | | |
260 | | /* Reduce empty-string to NULL case */ |
261 | 0 | if (comment != NULL && strlen(comment) == 0) |
262 | 0 | comment = NULL; |
263 | | |
264 | | /* Prepare to form or update a tuple, if necessary */ |
265 | 0 | if (comment != NULL) |
266 | 0 | { |
267 | 0 | for (i = 0; i < Natts_pg_shdescription; i++) |
268 | 0 | { |
269 | 0 | nulls[i] = false; |
270 | 0 | replaces[i] = true; |
271 | 0 | } |
272 | 0 | values[Anum_pg_shdescription_objoid - 1] = ObjectIdGetDatum(oid); |
273 | 0 | values[Anum_pg_shdescription_classoid - 1] = ObjectIdGetDatum(classoid); |
274 | 0 | values[Anum_pg_shdescription_description - 1] = CStringGetTextDatum(comment); |
275 | 0 | } |
276 | | |
277 | | /* Use the index to search for a matching old tuple */ |
278 | |
|
279 | 0 | ScanKeyInit(&skey[0], |
280 | 0 | Anum_pg_shdescription_objoid, |
281 | 0 | BTEqualStrategyNumber, F_OIDEQ, |
282 | 0 | ObjectIdGetDatum(oid)); |
283 | 0 | ScanKeyInit(&skey[1], |
284 | 0 | Anum_pg_shdescription_classoid, |
285 | 0 | BTEqualStrategyNumber, F_OIDEQ, |
286 | 0 | ObjectIdGetDatum(classoid)); |
287 | |
|
288 | 0 | shdescription = table_open(SharedDescriptionRelationId, RowExclusiveLock); |
289 | |
|
290 | 0 | sd = systable_beginscan(shdescription, SharedDescriptionObjIndexId, true, |
291 | 0 | NULL, 2, skey); |
292 | |
|
293 | 0 | while ((oldtuple = systable_getnext(sd)) != NULL) |
294 | 0 | { |
295 | | /* Found the old tuple, so delete or update it */ |
296 | |
|
297 | 0 | if (comment == NULL) |
298 | 0 | CatalogTupleDelete(shdescription, &oldtuple->t_self); |
299 | 0 | else |
300 | 0 | { |
301 | 0 | newtuple = heap_modify_tuple(oldtuple, RelationGetDescr(shdescription), |
302 | 0 | values, nulls, replaces); |
303 | 0 | CatalogTupleUpdate(shdescription, &oldtuple->t_self, newtuple); |
304 | 0 | } |
305 | |
|
306 | 0 | break; /* Assume there can be only one match */ |
307 | 0 | } |
308 | |
|
309 | 0 | systable_endscan(sd); |
310 | | |
311 | | /* If we didn't find an old tuple, insert a new one */ |
312 | |
|
313 | 0 | if (newtuple == NULL && comment != NULL) |
314 | 0 | { |
315 | 0 | newtuple = heap_form_tuple(RelationGetDescr(shdescription), |
316 | 0 | values, nulls); |
317 | 0 | CatalogTupleInsert(shdescription, newtuple); |
318 | 0 | } |
319 | |
|
320 | 0 | if (newtuple != NULL) |
321 | 0 | heap_freetuple(newtuple); |
322 | | |
323 | | /* Done */ |
324 | |
|
325 | 0 | table_close(shdescription, NoLock); |
326 | 0 | } |
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 | | DeleteComments(Oid oid, Oid classoid, int32 subid) |
337 | 0 | { |
338 | 0 | Relation description; |
339 | 0 | ScanKeyData skey[3]; |
340 | 0 | int nkeys; |
341 | 0 | SysScanDesc sd; |
342 | 0 | HeapTuple oldtuple; |
343 | | |
344 | | /* Use the index to search for all matching old tuples */ |
345 | |
|
346 | 0 | ScanKeyInit(&skey[0], |
347 | 0 | Anum_pg_description_objoid, |
348 | 0 | BTEqualStrategyNumber, F_OIDEQ, |
349 | 0 | ObjectIdGetDatum(oid)); |
350 | 0 | ScanKeyInit(&skey[1], |
351 | 0 | Anum_pg_description_classoid, |
352 | 0 | BTEqualStrategyNumber, F_OIDEQ, |
353 | 0 | ObjectIdGetDatum(classoid)); |
354 | |
|
355 | 0 | if (subid != 0) |
356 | 0 | { |
357 | 0 | ScanKeyInit(&skey[2], |
358 | 0 | Anum_pg_description_objsubid, |
359 | 0 | BTEqualStrategyNumber, F_INT4EQ, |
360 | 0 | Int32GetDatum(subid)); |
361 | 0 | nkeys = 3; |
362 | 0 | } |
363 | 0 | else |
364 | 0 | nkeys = 2; |
365 | |
|
366 | 0 | description = table_open(DescriptionRelationId, RowExclusiveLock); |
367 | |
|
368 | 0 | sd = systable_beginscan(description, DescriptionObjIndexId, true, |
369 | 0 | NULL, nkeys, skey); |
370 | |
|
371 | 0 | while ((oldtuple = systable_getnext(sd)) != NULL) |
372 | 0 | CatalogTupleDelete(description, &oldtuple->t_self); |
373 | | |
374 | | /* Done */ |
375 | |
|
376 | 0 | systable_endscan(sd); |
377 | 0 | table_close(description, RowExclusiveLock); |
378 | 0 | } |
379 | | |
380 | | /* |
381 | | * DeleteSharedComments -- remove comments for a shared object |
382 | | */ |
383 | | void |
384 | | DeleteSharedComments(Oid oid, Oid classoid) |
385 | 0 | { |
386 | 0 | Relation shdescription; |
387 | 0 | ScanKeyData skey[2]; |
388 | 0 | SysScanDesc sd; |
389 | 0 | HeapTuple oldtuple; |
390 | | |
391 | | /* Use the index to search for all matching old tuples */ |
392 | |
|
393 | 0 | ScanKeyInit(&skey[0], |
394 | 0 | Anum_pg_shdescription_objoid, |
395 | 0 | BTEqualStrategyNumber, F_OIDEQ, |
396 | 0 | ObjectIdGetDatum(oid)); |
397 | 0 | ScanKeyInit(&skey[1], |
398 | 0 | Anum_pg_shdescription_classoid, |
399 | 0 | BTEqualStrategyNumber, F_OIDEQ, |
400 | 0 | ObjectIdGetDatum(classoid)); |
401 | |
|
402 | 0 | shdescription = table_open(SharedDescriptionRelationId, RowExclusiveLock); |
403 | |
|
404 | 0 | sd = systable_beginscan(shdescription, SharedDescriptionObjIndexId, true, |
405 | 0 | NULL, 2, skey); |
406 | |
|
407 | 0 | while ((oldtuple = systable_getnext(sd)) != NULL) |
408 | 0 | CatalogTupleDelete(shdescription, &oldtuple->t_self); |
409 | | |
410 | | /* Done */ |
411 | |
|
412 | 0 | systable_endscan(sd); |
413 | 0 | table_close(shdescription, RowExclusiveLock); |
414 | 0 | } |
415 | | |
416 | | /* |
417 | | * GetComment -- get the comment for an object, or null if not found. |
418 | | */ |
419 | | char * |
420 | | GetComment(Oid oid, Oid classoid, int32 subid) |
421 | 0 | { |
422 | 0 | Relation description; |
423 | 0 | ScanKeyData skey[3]; |
424 | 0 | SysScanDesc sd; |
425 | 0 | TupleDesc tupdesc; |
426 | 0 | HeapTuple tuple; |
427 | 0 | char *comment; |
428 | | |
429 | | /* Use the index to search for a matching old tuple */ |
430 | |
|
431 | 0 | ScanKeyInit(&skey[0], |
432 | 0 | Anum_pg_description_objoid, |
433 | 0 | BTEqualStrategyNumber, F_OIDEQ, |
434 | 0 | ObjectIdGetDatum(oid)); |
435 | 0 | ScanKeyInit(&skey[1], |
436 | 0 | Anum_pg_description_classoid, |
437 | 0 | BTEqualStrategyNumber, F_OIDEQ, |
438 | 0 | ObjectIdGetDatum(classoid)); |
439 | 0 | ScanKeyInit(&skey[2], |
440 | 0 | Anum_pg_description_objsubid, |
441 | 0 | BTEqualStrategyNumber, F_INT4EQ, |
442 | 0 | Int32GetDatum(subid)); |
443 | |
|
444 | 0 | description = table_open(DescriptionRelationId, AccessShareLock); |
445 | 0 | tupdesc = RelationGetDescr(description); |
446 | |
|
447 | 0 | sd = systable_beginscan(description, DescriptionObjIndexId, true, |
448 | 0 | NULL, 3, skey); |
449 | |
|
450 | 0 | comment = NULL; |
451 | 0 | while ((tuple = systable_getnext(sd)) != NULL) |
452 | 0 | { |
453 | 0 | Datum value; |
454 | 0 | bool isnull; |
455 | | |
456 | | /* Found the tuple, get description field */ |
457 | 0 | value = heap_getattr(tuple, Anum_pg_description_description, tupdesc, &isnull); |
458 | 0 | if (!isnull) |
459 | 0 | comment = TextDatumGetCString(value); |
460 | 0 | break; /* Assume there can be only one match */ |
461 | 0 | } |
462 | |
|
463 | 0 | systable_endscan(sd); |
464 | | |
465 | | /* Done */ |
466 | 0 | table_close(description, AccessShareLock); |
467 | |
|
468 | 0 | return comment; |
469 | 0 | } |