Coverage Report

Created: 2025-06-15 06:31

/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
}