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