/*-------------------------------------------------------------------------
 *
 * comment.c
 *
 * PostgreSQL object comments utility code.
 *
 * Copyright (c) 1999-2001, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *	  $Header: /cvsroot/pgsql/src/backend/commands/comment.c,v 1.31 2001/06/25 21:11:43 tgl Exp $
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/heapam.h"
#include "catalog/catname.h"
#include "catalog/indexing.h"
#include "catalog/pg_database.h"
#include "catalog/pg_description.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "catalog/pg_class.h"
#include "commands/comment.h"
#include "miscadmin.h"
#include "parser/parse_expr.h"
#include "parser/parse_func.h"
#include "parser/parse.h"
#include "rewrite/rewriteRemove.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/syscache.h"


/*------------------------------------------------------------------
 * Static Function Prototypes --
 *
 * The following protoypes are declared static so as not to conflict
 * with any other routines outside this module. These routines are
 * called by the public function CommentObject() routine to create
 * the appropriate comment for the specific object type.
 *------------------------------------------------------------------
 */

static void CommentRelation(int objtype, char *relation, char *comment);
static void CommentAttribute(char *relation, char *attrib, char *comment);
static void CommentDatabase(char *database, char *comment);
static void CommentRewrite(char *rule, char *comment);
static void CommentType(char *type, char *comment);
static void CommentAggregate(char *aggregate, List *arguments, char *comment);
static void CommentProc(char *function, List *arguments, char *comment);
static void CommentOperator(char *opname, List *arguments, char *comment);
static void CommentTrigger(char *trigger, char *relation, char *comments);
static void CreateComments(Oid oid, char *comment);

/*------------------------------------------------------------------
 * CommentObject --
 *
 * This routine is used to add the associated comment into
 * pg_description for the object specified by the paramters handed
 * to this routine. If the routine cannot determine an Oid to
 * associated with the parameters handed to this routine, an
 * error is thrown. Otherwise the comment is added to pg_description
 * by calling the CreateComments() routine. If the comments were
 * empty, CreateComments() will drop any comments associated with
 * the object.
 *------------------------------------------------------------------
*/

void
CommentObject(int objtype, char *objname, char *objproperty,
			  List *objlist, char *comment)
{

	switch (objtype)
	{
			case (INDEX):
			case (SEQUENCE):
			case (TABLE):
			case (VIEW):
			CommentRelation(objtype, objname, comment);
			break;
		case (COLUMN):
			CommentAttribute(objname, objproperty, comment);
			break;
		case (DATABASE):
			CommentDatabase(objname, comment);
			break;
		case (RULE):
			CommentRewrite(objname, comment);
			break;
		case (TYPE_P):
			CommentType(objname, comment);
			break;
		case (AGGREGATE):
			CommentAggregate(objname, objlist, comment);
			break;
		case (FUNCTION):
			CommentProc(objname, objlist, comment);
			break;
		case (OPERATOR):
			CommentOperator(objname, objlist, comment);
			break;
		case (TRIGGER):
			CommentTrigger(objname, objproperty, comment);
			break;
		default:
			elog(ERROR, "An attempt was made to comment on a unknown type: %i",
				 objtype);
	}


}

/*------------------------------------------------------------------
 * CreateComments --
 *
 * This routine is handed the oid and the command associated
 * with that id and will insert, update, or delete (if the
 * comment is an empty string or a NULL pointer) the associated
 * comment from the system cataloge, pg_description.
 *
 *------------------------------------------------------------------
 */

static void
CreateComments(Oid oid, char *comment)
{

	Relation	description;
	TupleDesc	tupDesc;
	HeapScanDesc scan;
	ScanKeyData entry;
	HeapTuple	desctuple = NULL,
				searchtuple;
	Datum		values[Natts_pg_description];
	char		nulls[Natts_pg_description];
	char		replaces[Natts_pg_description];
	bool		modified = false;
	int			i;

	/*** Open pg_description, form a new tuple, if necessary ***/

	description = heap_openr(DescriptionRelationName, RowExclusiveLock);
	tupDesc = description->rd_att;
	if ((comment != NULL) && (strlen(comment) > 0))
	{
		for (i = 0; i < Natts_pg_description; i++)
		{
			nulls[i] = ' ';
			replaces[i] = 'r';
			values[i] = (Datum) NULL;
		}
		i = 0;
		values[i++] = ObjectIdGetDatum(oid);
		values[i++] = DirectFunctionCall1(textin, CStringGetDatum(comment));
	}

	/*** Now, open pg_description and attempt to find the old tuple ***/

	ScanKeyEntryInitialize(&entry, 0x0, Anum_pg_description_objoid, F_OIDEQ,
						   ObjectIdGetDatum(oid));
	scan = heap_beginscan(description, false, SnapshotNow, 1, &entry);
	searchtuple = heap_getnext(scan, 0);

	/*** If a previous tuple exists, either delete or prep replacement ***/

	if (HeapTupleIsValid(searchtuple))
	{

		/*** If the comment is blank, delete old entry, else update it ***/

		if ((comment == NULL) || (strlen(comment) == 0))
			simple_heap_delete(description, &searchtuple->t_self);
		else
		{
			desctuple = heap_modifytuple(searchtuple, description, values,
										 nulls, replaces);
			simple_heap_update(description, &searchtuple->t_self, desctuple);
			modified = TRUE;
		}

	}
	else
	{

		/*** Only if comment is non-blank do we form a new tuple ***/

		if ((comment != NULL) && (strlen(comment) > 0))
		{
			desctuple = heap_formtuple(tupDesc, values, nulls);
			heap_insert(description, desctuple);
			modified = TRUE;
		}

	}

	/*** Complete the scan, update indices, if necessary ***/

	heap_endscan(scan);

	if (modified)
	{
		if (RelationGetForm(description)->relhasindex)
		{
			Relation	idescs[Num_pg_description_indices];

			CatalogOpenIndices(Num_pg_description_indices,
							   Name_pg_description_indices, idescs);
			CatalogIndexInsert(idescs, Num_pg_description_indices, description,
							   desctuple);
			CatalogCloseIndices(Num_pg_description_indices, idescs);
		}
		heap_freetuple(desctuple);

	}

	heap_close(description, RowExclusiveLock);

}

/*------------------------------------------------------------------
 * DeleteComments --
 *
 * This routine is used to purge any comments
 * associated with the Oid handed to this routine,
 * regardless of the actual object type. It is
 * called, for example, when a relation is destroyed.
 *------------------------------------------------------------------
 */

void
DeleteComments(Oid oid)
{

	Relation	description;
	TupleDesc	tupDesc;
	ScanKeyData entry;
	HeapScanDesc scan;
	HeapTuple	searchtuple;

	description = heap_openr(DescriptionRelationName, RowExclusiveLock);
	tupDesc = description->rd_att;

	/*** Now, open pg_description and attempt to find the old tuple ***/

	ScanKeyEntryInitialize(&entry, 0x0, Anum_pg_description_objoid, F_OIDEQ,
						   ObjectIdGetDatum(oid));
	scan = heap_beginscan(description, false, SnapshotNow, 1, &entry);
	searchtuple = heap_getnext(scan, 0);

	/*** If a previous tuple exists, delete it ***/

	if (HeapTupleIsValid(searchtuple))
		simple_heap_delete(description, &searchtuple->t_self);

	/*** Complete the scan, update indices, if necessary ***/

	heap_endscan(scan);
	heap_close(description, RowExclusiveLock);

}

/*------------------------------------------------------------------
 * CommentRelation --
 *
 * This routine is used to add/drop a comment from a relation, where
 * a relation is a TABLE, SEQUENCE, VIEW or INDEX. The routine simply
 * finds the relation name by searching the system cache, locating
 * the appropriate tuple, and inserting a comment using that
 * tuple's oid. Its parameters are the relation name and comments.
 *------------------------------------------------------------------
*/

static void
CommentRelation(int reltype, char *relname, char *comment)
{
	HeapTuple	reltuple;
	Oid			oid;
	char		relkind;

	/*** First, check object security ***/

	if (!pg_ownercheck(GetUserId(), relname, RELNAME))
		elog(ERROR, "you are not permitted to comment on class '%s'", relname);

	/*** Now, attempt to find the oid in the cached version of pg_class ***/

	reltuple = SearchSysCache(RELNAME,
							  PointerGetDatum(relname),
							  0, 0, 0);
	if (!HeapTupleIsValid(reltuple))
		elog(ERROR, "relation '%s' does not exist", relname);

	oid = reltuple->t_data->t_oid;

	relkind = ((Form_pg_class) GETSTRUCT(reltuple))->relkind;

	ReleaseSysCache(reltuple);

	/*** Next, verify that the relation type matches the intent ***/

	switch (reltype)
	{
		case (INDEX):
			if (relkind != RELKIND_INDEX)
				elog(ERROR, "relation '%s' is not an index", relname);
			break;
		case (TABLE):
			if (relkind != RELKIND_RELATION)
				elog(ERROR, "relation '%s' is not a table", relname);
			break;
		case (VIEW):
			if (relkind != RELKIND_VIEW)
				elog(ERROR, "relation '%s' is not a view", relname);
			break;
		case (SEQUENCE):
			if (relkind != RELKIND_SEQUENCE)
				elog(ERROR, "relation '%s' is not a sequence", relname);
			break;
	}

	/*** Create the comments using the tuple's oid ***/

	CreateComments(oid, comment);
}

/*------------------------------------------------------------------
 * CommentAttribute --
 *
 * This routine is used to add/drop a comment from an attribute
 * such as a table's column. The routine will check security
 * restrictions and then attempt to fetch the oid of the associated
 * attribute. If successful, a comment is added/dropped, else an
 * elog() exception is thrown.	The parameters are the relation
 * and attribute names, and the comments
 *------------------------------------------------------------------
*/

static void
CommentAttribute(char *relname, char *attrname, char *comment)
{
	Relation	relation;
	Oid			oid;

	/*** First, check object security ***/

	if (!pg_ownercheck(GetUserId(), relname, RELNAME))
		elog(ERROR, "you are not permitted to comment on class '%s\'", relname);

	/* Open the containing relation to ensure it won't go away meanwhile */

	relation = heap_openr(relname, AccessShareLock);

	/*** Now, fetch the attribute oid from the system cache ***/

	oid = GetSysCacheOid(ATTNAME,
						 ObjectIdGetDatum(relation->rd_id),
						 PointerGetDatum(attrname),
						 0, 0);
	if (!OidIsValid(oid))
		elog(ERROR, "'%s' is not an attribute of class '%s'",
			 attrname, relname);

	/*** Call CreateComments() to create/drop the comments ***/

	CreateComments(oid, comment);

	/*** Now, close the heap relation and return ***/

	heap_close(relation, NoLock);
}

/*------------------------------------------------------------------
 * CommentDatabase --
 *
 * This routine is used to add/drop any user-comments a user might
 * have regarding the specified database. The routine will check
 * security for owner permissions, and, if succesful, will then
 * attempt to find the oid of the database specified. Once found,
 * a comment is added/dropped using the CreateComments() routine.
 *------------------------------------------------------------------
*/

static void
CommentDatabase(char *database, char *comment)
{
	Relation	pg_database;
	ScanKeyData entry;
	HeapScanDesc scan;
	HeapTuple	dbtuple;
	Oid			oid;

	/*** First find the tuple in pg_database for the database ***/

	pg_database = heap_openr(DatabaseRelationName, AccessShareLock);
	ScanKeyEntryInitialize(&entry, 0, Anum_pg_database_datname,
						   F_NAMEEQ, NameGetDatum(database));
	scan = heap_beginscan(pg_database, 0, SnapshotNow, 1, &entry);
	dbtuple = heap_getnext(scan, 0);

	/*** Validate database exists, and fetch the db oid ***/

	if (!HeapTupleIsValid(dbtuple))
		elog(ERROR, "database '%s' does not exist", database);
	oid = dbtuple->t_data->t_oid;

	/*** Allow if the user matches the database dba or is a superuser ***/

	if (!(superuser() || is_dbadmin(oid)))
		elog(ERROR, "you are not permitted to comment on database '%s'",
			 database);

	/*** Create the comments with the pg_database oid ***/

	CreateComments(oid, comment);

	/*** Complete the scan and close any opened relations ***/

	heap_endscan(scan);
	heap_close(pg_database, AccessShareLock);
}

/*------------------------------------------------------------------
 * CommentRewrite --
 *
 * This routine is used to add/drop any user-comments a user might
 * have regarding a specified RULE. The rule is specified by name
 * and, if found, and the user has appropriate permissions, a
 * comment will be added/dropped using the CreateComments() routine.
 *------------------------------------------------------------------
*/

static void
CommentRewrite(char *rule, char *comment)
{
	Oid			oid;
	char	   *relation;
	int			aclcheck;

	/*** First, validate user ***/

#ifndef NO_SECURITY
	relation = RewriteGetRuleEventRel(rule);
	aclcheck = pg_aclcheck(relation, GetUserId(), ACL_RULE);
	if (aclcheck != ACLCHECK_OK)
	{
		elog(ERROR, "you are not permitted to comment on rule '%s'",
			 rule);
	}
#endif

	/*** Next, find the rule's oid ***/

	oid = GetSysCacheOid(RULENAME,
						 PointerGetDatum(rule),
						 0, 0, 0);
	if (!OidIsValid(oid))
		elog(ERROR, "rule '%s' does not exist", rule);

	/*** Call CreateComments() to create/drop the comments ***/

	CreateComments(oid, comment);
}

/*------------------------------------------------------------------
 * CommentType --
 *
 * This routine is used to add/drop any user-comments a user might
 * have regarding a TYPE. The type is specified by name
 * and, if found, and the user has appropriate permissions, a
 * comment will be added/dropped using the CreateComments() routine.
 * The type's name and the comments are the paramters to this routine.
 *------------------------------------------------------------------
*/

static void
CommentType(char *type, char *comment)
{
	Oid			oid;

	/*** First, validate user ***/

	if (!pg_ownercheck(GetUserId(), type, TYPENAME))
		elog(ERROR, "you are not permitted to comment on type '%s'",
			 type);

	/*** Next, find the type's oid ***/

	oid = GetSysCacheOid(TYPENAME,
						 PointerGetDatum(type),
						 0, 0, 0);
	if (!OidIsValid(oid))
		elog(ERROR, "type '%s' does not exist", type);

	/*** Call CreateComments() to create/drop the comments ***/

	CreateComments(oid, comment);
}

/*------------------------------------------------------------------
 * CommentAggregate --
 *
 * This routine is used to allow a user to provide comments on an
 * aggregate function. The aggregate function is determined by both
 * its name and its argument type, which, with the comments are
 * the three parameters handed to this routine.
 *------------------------------------------------------------------
*/

static void
CommentAggregate(char *aggregate, List *arguments, char *comment)
{
	TypeName   *aggtype = (TypeName *) lfirst(arguments);
	char	   *aggtypename = NULL;
	Oid			baseoid,
				oid;
	bool		defined;

	/*** First, attempt to determine the base aggregate oid ***/

	if (aggtype)
	{
		aggtypename = TypeNameToInternalName(aggtype);
		baseoid = TypeGet(aggtypename, &defined);
		if (!OidIsValid(baseoid))
			elog(ERROR, "type '%s' does not exist", aggtypename);
	}
	else
		baseoid = 0;

	/*** Next, validate the user's attempt to comment ***/

	if (!pg_aggr_ownercheck(GetUserId(), aggregate, baseoid))
	{
		if (aggtypename)
			elog(ERROR, "you are not permitted to comment on aggregate '%s' %s '%s'",
				 aggregate, "with type", aggtypename);
		else
			elog(ERROR, "you are not permitted to comment on aggregate '%s'",
				 aggregate);
	}

	/*** Now, attempt to find the actual tuple in pg_aggregate ***/

	oid = GetSysCacheOid(AGGNAME,
						 PointerGetDatum(aggregate),
						 ObjectIdGetDatum(baseoid),
						 0, 0);
	if (!OidIsValid(oid))
	{
		if (aggtypename)
		{
			elog(ERROR, "aggregate type '%s' does not exist for aggregate '%s'",
				 aggtypename, aggregate);
		}
		else
			elog(ERROR, "aggregate '%s' does not exist", aggregate);
	}

	/*** Call CreateComments() to create/drop the comments ***/

	CreateComments(oid, comment);
}

/*------------------------------------------------------------------
 * CommentProc --
 *
 * This routine is used to allow a user to provide comments on an
 * procedure (function). The procedure is determined by both
 * its name and its argument list. The argument list is expected to
 * be a series of parsed nodes pointed to by a List object. If the
 * comments string is empty, the associated comment is dropped.
 *------------------------------------------------------------------
*/

static void
CommentProc(char *function, List *arguments, char *comment)
{
	Oid			oid,
				argoids[FUNC_MAX_ARGS];
	int			i,
				argcount;

	/*** First, initialize function's argument list with their type oids ***/

	MemSet(argoids, 0, FUNC_MAX_ARGS * sizeof(Oid));
	argcount = length(arguments);
	if (argcount > FUNC_MAX_ARGS)
		elog(ERROR, "functions cannot have more than %d arguments",
			 FUNC_MAX_ARGS);
	for (i = 0; i < argcount; i++)
	{
		TypeName   *t = (TypeName *) lfirst(arguments);
		char	   *typnam = TypeNameToInternalName(t);

		arguments = lnext(arguments);

		if (strcmp(typnam, "opaque") == 0)
			argoids[i] = InvalidOid;
		else
		{
			argoids[i] = GetSysCacheOid(TYPENAME,
										PointerGetDatum(typnam),
										0, 0, 0);
			if (!OidIsValid(argoids[i]))
				elog(ERROR, "CommentProc: type '%s' not found", typnam);
		}
	}

	/*** Now, validate the user's ability to comment on this function ***/

	if (!pg_func_ownercheck(GetUserId(), function, argcount, argoids))
		elog(ERROR, "you are not permitted to comment on function '%s'",
			 function);

	/*** Now, find the corresponding oid for this procedure ***/

	oid = GetSysCacheOid(PROCNAME,
						 PointerGetDatum(function),
						 Int32GetDatum(argcount),
						 PointerGetDatum(argoids),
						 0);
	if (!OidIsValid(oid))
		func_error("CommentProc", function, argcount, argoids, NULL);

	/*** Call CreateComments() to create/drop the comments ***/

	CreateComments(oid, comment);
}

/*------------------------------------------------------------------
 * CommentOperator --
 *
 * This routine is used to allow a user to provide comments on an
 * operator. The operator for commenting is determined by both
 * its name and its argument list which defines the left and right
 * hand types the operator will operate on. The argument list is
 * expected to be a couple of parse nodes pointed to be a List
 * object. If the comments string is empty, the associated comment
 * is dropped.
 *------------------------------------------------------------------
*/

static void
CommentOperator(char *opername, List *arguments, char *comment)
{
	TypeName   *typenode1 = (TypeName *) lfirst(arguments);
	TypeName   *typenode2 = (TypeName *) lsecond(arguments);
	char		oprtype = 0,
			   *lefttype = NULL,
			   *righttype = NULL;
	Form_pg_operator data;
	HeapTuple	optuple;
	Oid			oid,
				leftoid = InvalidOid,
				rightoid = InvalidOid;
	bool		defined;

	/*** Initialize our left and right argument types ***/

	if (typenode1 != NULL)
		lefttype = TypeNameToInternalName(typenode1);
	if (typenode2 != NULL)
		righttype = TypeNameToInternalName(typenode2);

	/*** Attempt to fetch the left oid, if specified ***/

	if (lefttype != NULL)
	{
		leftoid = TypeGet(lefttype, &defined);
		if (!OidIsValid(leftoid))
			elog(ERROR, "left type '%s' does not exist", lefttype);
	}

	/*** Attempt to fetch the right oid, if specified ***/

	if (righttype != NULL)
	{
		rightoid = TypeGet(righttype, &defined);
		if (!OidIsValid(rightoid))
			elog(ERROR, "right type '%s' does not exist", righttype);
	}

	/*** Determine operator type ***/

	if (OidIsValid(leftoid) && (OidIsValid(rightoid)))
		oprtype = 'b';
	else if (OidIsValid(leftoid))
		oprtype = 'r';
	else if (OidIsValid(rightoid))
		oprtype = 'l';
	else
		elog(ERROR, "operator '%s' is of an illegal type'", opername);

	/*** Attempt to fetch the operator oid ***/

	optuple = SearchSysCache(OPERNAME,
							 PointerGetDatum(opername),
							 ObjectIdGetDatum(leftoid),
							 ObjectIdGetDatum(rightoid),
							 CharGetDatum(oprtype));
	if (!HeapTupleIsValid(optuple))
		elog(ERROR, "operator '%s' does not exist", opername);

	oid = optuple->t_data->t_oid;

	/*** Valid user's ability to comment on this operator ***/

	if (!pg_oper_ownercheck(GetUserId(), oid))
		elog(ERROR, "you are not permitted to comment on operator '%s'",
			 opername);

	/*** Get the procedure associated with the operator ***/

	data = (Form_pg_operator) GETSTRUCT(optuple);
	oid = data->oprcode;
	if (oid == InvalidOid)
		elog(ERROR, "operator '%s' does not have an underlying function", opername);

	ReleaseSysCache(optuple);

	/*** Call CreateComments() to create/drop the comments ***/

	CreateComments(oid, comment);
}

/*------------------------------------------------------------------
 * CommentTrigger --
 *
 * This routine is used to allow a user to provide comments on a
 * trigger event. The trigger for commenting is determined by both
 * its name and the relation to which it refers. The arguments to this
 * function are the trigger name, the relation name, and the comments
 * to add/drop.
 *------------------------------------------------------------------
*/

static void
CommentTrigger(char *trigger, char *relname, char *comment)
{

	Form_pg_trigger data;
	Relation	pg_trigger,
				relation;
	HeapTuple	triggertuple;
	HeapScanDesc scan;
	ScanKeyData entry;
	Oid			oid = InvalidOid;

	/*** First, validate the user's action ***/

	if (!pg_ownercheck(GetUserId(), relname, RELNAME))
		elog(ERROR, "you are not permitted to comment on trigger '%s' %s '%s'",
			 trigger, "defined for relation", relname);

	/*** Now, fetch the trigger oid from pg_trigger  ***/

	relation = heap_openr(relname, AccessShareLock);
	pg_trigger = heap_openr(TriggerRelationName, AccessShareLock);
	ScanKeyEntryInitialize(&entry, 0, Anum_pg_trigger_tgrelid,
						   F_OIDEQ, RelationGetRelid(relation));
	scan = heap_beginscan(pg_trigger, 0, SnapshotNow, 1, &entry);
	triggertuple = heap_getnext(scan, 0);
	while (HeapTupleIsValid(triggertuple))
	{
		data = (Form_pg_trigger) GETSTRUCT(triggertuple);
		if (namestrcmp(&(data->tgname), trigger) == 0)
		{
			oid = triggertuple->t_data->t_oid;
			break;
		}
		triggertuple = heap_getnext(scan, 0);
	}

	/*** If no trigger exists for the relation specified, notify user ***/

	if (oid == InvalidOid)
	{
		elog(ERROR, "trigger '%s' defined for relation '%s' does not exist",
			 trigger, relname);
	}

	/*** Create the comments with the pg_trigger oid ***/

	CreateComments(oid, comment);

	/*** Complete the scan and close any opened relations ***/

	heap_endscan(scan);
	heap_close(pg_trigger, AccessShareLock);
	heap_close(relation, NoLock);
}
