Coverage Report

Created: 2026-11-12 07:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}