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