/*-------------------------------------------------------------------------
 *
 * collationcmds.c
 *	  collation-related commands support code
 *
 * Portions Copyright (c) 1996-2011, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/commands/collationcmds.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/heapam.h"
#include "access/xact.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/namespace.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_collation_fn.h"
#include "commands/alter.h"
#include "commands/collationcmds.h"
#include "commands/dbcommands.h"
#include "commands/defrem.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/pg_locale.h"
#include "utils/syscache.h"

static void AlterCollationOwner_internal(Relation rel, Oid collationOid,
							 Oid newOwnerId);

/*
 * CREATE COLLATION
 */
void
DefineCollation(List *names, List *parameters)
{
	char	   *collName;
	Oid			collNamespace;
	AclResult	aclresult;
	ListCell   *pl;
	DefElem    *fromEl = NULL;
	DefElem    *localeEl = NULL;
	DefElem    *lccollateEl = NULL;
	DefElem    *lcctypeEl = NULL;
	char	   *collcollate = NULL;
	char	   *collctype = NULL;
	Oid			newoid;

	collNamespace = QualifiedNameGetCreationNamespace(names, &collName);

	aclresult = pg_namespace_aclcheck(collNamespace, GetUserId(), ACL_CREATE);
	if (aclresult != ACLCHECK_OK)
		aclcheck_error(aclresult, ACL_KIND_NAMESPACE,
					   get_namespace_name(collNamespace));

	foreach(pl, parameters)
	{
		DefElem    *defel = (DefElem *) lfirst(pl);
		DefElem   **defelp;

		if (pg_strcasecmp(defel->defname, "from") == 0)
			defelp = &fromEl;
		else if (pg_strcasecmp(defel->defname, "locale") == 0)
			defelp = &localeEl;
		else if (pg_strcasecmp(defel->defname, "lc_collate") == 0)
			defelp = &lccollateEl;
		else if (pg_strcasecmp(defel->defname, "lc_ctype") == 0)
			defelp = &lcctypeEl;
		else
		{
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("collation attribute \"%s\" not recognized",
							defel->defname)));
			break;
		}

		*defelp = defel;
	}

	if ((localeEl && (lccollateEl || lcctypeEl))
		|| (fromEl && list_length(parameters) != 1))
		ereport(ERROR,
				(errcode(ERRCODE_SYNTAX_ERROR),
				 errmsg("conflicting or redundant options")));

	if (fromEl)
	{
		Oid			collid;
		HeapTuple	tp;

		collid = get_collation_oid(defGetQualifiedName(fromEl), false);
		tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(collid));
		if (!HeapTupleIsValid(tp))
			elog(ERROR, "cache lookup failed for collation %u", collid);

		collcollate = pstrdup(NameStr(((Form_pg_collation) GETSTRUCT(tp))->collcollate));
		collctype = pstrdup(NameStr(((Form_pg_collation) GETSTRUCT(tp))->collctype));

		ReleaseSysCache(tp);
	}

	if (localeEl)
	{
		collcollate = defGetString(localeEl);
		collctype = defGetString(localeEl);
	}

	if (lccollateEl)
		collcollate = defGetString(lccollateEl);

	if (lcctypeEl)
		collctype = defGetString(lcctypeEl);

	if (!collcollate)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
				 errmsg("parameter \"lc_collate\" must be specified")));

	if (!collctype)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
				 errmsg("parameter \"lc_ctype\" must be specified")));

	check_encoding_locale_matches(GetDatabaseEncoding(), collcollate, collctype);

	newoid = CollationCreate(collName,
							 collNamespace,
							 GetUserId(),
							 GetDatabaseEncoding(),
							 collcollate,
							 collctype);

	/* check that the locales can be loaded */
	CommandCounterIncrement();
	(void) pg_newlocale_from_collation(newoid);
}

/*
 * DROP COLLATION
 */
void
DropCollationsCommand(DropStmt *drop)
{
	ObjectAddresses *objects;
	ListCell   *cell;

	/*
	 * First we identify all the collations, then we delete them in a single
	 * performMultipleDeletions() call.  This is to avoid unwanted DROP
	 * RESTRICT errors if one of the collations depends on another. (Not that
	 * that is very likely, but we may as well do this consistently.)
	 */
	objects = new_object_addresses();

	foreach(cell, drop->objects)
	{
		List	   *name = (List *) lfirst(cell);
		Oid			collationOid;
		HeapTuple	tuple;
		Form_pg_collation coll;
		ObjectAddress object;

		collationOid = get_collation_oid(name, drop->missing_ok);

		if (!OidIsValid(collationOid))
		{
			ereport(NOTICE,
					(errmsg("collation \"%s\" does not exist, skipping",
							NameListToString(name))));
			continue;
		}

		tuple = SearchSysCache1(COLLOID, ObjectIdGetDatum(collationOid));
		if (!HeapTupleIsValid(tuple))
			elog(ERROR, "cache lookup failed for collation %u",
				 collationOid);
		coll = (Form_pg_collation) GETSTRUCT(tuple);

		/* Permission check: must own collation or its namespace */
		if (!pg_collation_ownercheck(collationOid, GetUserId()) &&
			!pg_namespace_ownercheck(coll->collnamespace, GetUserId()))
			aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_COLLATION,
						   NameStr(coll->collname));

		object.classId = CollationRelationId;
		object.objectId = collationOid;
		object.objectSubId = 0;

		add_exact_object_address(&object, objects);

		ReleaseSysCache(tuple);
	}

	performMultipleDeletions(objects, drop->behavior);

	free_object_addresses(objects);
}

/*
 * Rename collation
 */
void
RenameCollation(List *name, const char *newname)
{
	Oid			collationOid;
	Oid			namespaceOid;
	HeapTuple	tup;
	Relation	rel;
	AclResult	aclresult;

	rel = heap_open(CollationRelationId, RowExclusiveLock);

	collationOid = get_collation_oid(name, false);

	tup = SearchSysCacheCopy1(COLLOID, ObjectIdGetDatum(collationOid));
	if (!HeapTupleIsValid(tup)) /* should not happen */
		elog(ERROR, "cache lookup failed for collation %u", collationOid);

	namespaceOid = ((Form_pg_collation) GETSTRUCT(tup))->collnamespace;

	/* make sure the new name doesn't exist */
	if (SearchSysCacheExists3(COLLNAMEENCNSP,
							  CStringGetDatum(newname),
							  Int32GetDatum(GetDatabaseEncoding()),
							  ObjectIdGetDatum(namespaceOid)))
		ereport(ERROR,
				(errcode(ERRCODE_DUPLICATE_OBJECT),
				 errmsg("collation \"%s\" for encoding \"%s\" already exists in schema \"%s\"",
						newname,
						GetDatabaseEncodingName(),
						get_namespace_name(namespaceOid))));

	/* mustn't match an any-encoding entry, either */
	if (SearchSysCacheExists3(COLLNAMEENCNSP,
							  CStringGetDatum(newname),
							  Int32GetDatum(-1),
							  ObjectIdGetDatum(namespaceOid)))
		ereport(ERROR,
				(errcode(ERRCODE_DUPLICATE_OBJECT),
				 errmsg("collation \"%s\" already exists in schema \"%s\"",
						newname,
						get_namespace_name(namespaceOid))));

	/* must be owner */
	if (!pg_collation_ownercheck(collationOid, GetUserId()))
		aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_COLLATION,
					   NameListToString(name));

	/* must have CREATE privilege on namespace */
	aclresult = pg_namespace_aclcheck(namespaceOid, GetUserId(), ACL_CREATE);
	if (aclresult != ACLCHECK_OK)
		aclcheck_error(aclresult, ACL_KIND_NAMESPACE,
					   get_namespace_name(namespaceOid));

	/* rename */
	namestrcpy(&(((Form_pg_collation) GETSTRUCT(tup))->collname), newname);
	simple_heap_update(rel, &tup->t_self, tup);
	CatalogUpdateIndexes(rel, tup);

	heap_freetuple(tup);

	heap_close(rel, RowExclusiveLock);
}

/*
 * Change collation owner, by name
 */
void
AlterCollationOwner(List *name, Oid newOwnerId)
{
	Oid			collationOid;
	Relation	rel;

	rel = heap_open(CollationRelationId, RowExclusiveLock);

	collationOid = get_collation_oid(name, false);

	AlterCollationOwner_internal(rel, collationOid, newOwnerId);

	heap_close(rel, RowExclusiveLock);
}

/*
 * Change collation owner, by oid
 */
void
AlterCollationOwner_oid(Oid collationOid, Oid newOwnerId)
{
	Relation	rel;

	rel = heap_open(CollationRelationId, RowExclusiveLock);

	AlterCollationOwner_internal(rel, collationOid, newOwnerId);

	heap_close(rel, RowExclusiveLock);
}

/*
 * AlterCollationOwner_internal
 *
 * Internal routine for changing the owner.  rel must be pg_collation, already
 * open and suitably locked; it will not be closed.
 */
static void
AlterCollationOwner_internal(Relation rel, Oid collationOid, Oid newOwnerId)
{
	Form_pg_collation collForm;
	HeapTuple	tup;

	Assert(RelationGetRelid(rel) == CollationRelationId);

	tup = SearchSysCacheCopy1(COLLOID, ObjectIdGetDatum(collationOid));
	if (!HeapTupleIsValid(tup)) /* should not happen */
		elog(ERROR, "cache lookup failed for collation %u", collationOid);

	collForm = (Form_pg_collation) GETSTRUCT(tup);

	/*
	 * If the new owner is the same as the existing owner, consider the
	 * command to have succeeded.  This is for dump restoration purposes.
	 */
	if (collForm->collowner != newOwnerId)
	{
		AclResult	aclresult;

		/* Superusers can always do it */
		if (!superuser())
		{
			/* Otherwise, must be owner of the existing object */
			if (!pg_collation_ownercheck(HeapTupleGetOid(tup), GetUserId()))
				aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_COLLATION,
							   NameStr(collForm->collname));

			/* Must be able to become new owner */
			check_is_member_of_role(GetUserId(), newOwnerId);

			/* New owner must have CREATE privilege on namespace */
			aclresult = pg_namespace_aclcheck(collForm->collnamespace,
											  newOwnerId,
											  ACL_CREATE);
			if (aclresult != ACLCHECK_OK)
				aclcheck_error(aclresult, ACL_KIND_NAMESPACE,
							   get_namespace_name(collForm->collnamespace));
		}

		/*
		 * Modify the owner --- okay to scribble on tup because it's a copy
		 */
		collForm->collowner = newOwnerId;

		simple_heap_update(rel, &tup->t_self, tup);

		CatalogUpdateIndexes(rel, tup);

		/* Update owner dependency reference */
		changeDependencyOnOwner(CollationRelationId, collationOid,
								newOwnerId);
	}

	heap_freetuple(tup);
}

/*
 * Execute ALTER COLLATION SET SCHEMA
 */
void
AlterCollationNamespace(List *name, const char *newschema)
{
	Oid			collOid,
				nspOid;

	collOid = get_collation_oid(name, false);

	nspOid = LookupCreationNamespace(newschema);

	AlterCollationNamespace_oid(collOid, nspOid);
}

/*
 * Change collation schema, by oid
 */
Oid
AlterCollationNamespace_oid(Oid collOid, Oid newNspOid)
{
	Oid			oldNspOid;
	Relation	rel;
	char	   *collation_name;

	rel = heap_open(CollationRelationId, RowExclusiveLock);

	/*
	 * We have to check for name collision ourselves, because
	 * AlterObjectNamespace doesn't know how to deal with the encoding
	 * considerations.
	 */
	collation_name = get_collation_name(collOid);
	if (!collation_name)
		elog(ERROR, "cache lookup failed for collation %u", collOid);

	/* make sure the name doesn't already exist in new schema */
	if (SearchSysCacheExists3(COLLNAMEENCNSP,
							  CStringGetDatum(collation_name),
							  Int32GetDatum(GetDatabaseEncoding()),
							  ObjectIdGetDatum(newNspOid)))
		ereport(ERROR,
				(errcode(ERRCODE_DUPLICATE_OBJECT),
				 errmsg("collation \"%s\" for encoding \"%s\" already exists in schema \"%s\"",
						collation_name,
						GetDatabaseEncodingName(),
						get_namespace_name(newNspOid))));

	/* mustn't match an any-encoding entry, either */
	if (SearchSysCacheExists3(COLLNAMEENCNSP,
							  CStringGetDatum(collation_name),
							  Int32GetDatum(-1),
							  ObjectIdGetDatum(newNspOid)))
		ereport(ERROR,
				(errcode(ERRCODE_DUPLICATE_OBJECT),
				 errmsg("collation \"%s\" already exists in schema \"%s\"",
						collation_name,
						get_namespace_name(newNspOid))));

	/* OK, do the work */
	oldNspOid = AlterObjectNamespace(rel, COLLOID, -1,
									 collOid, newNspOid,
									 Anum_pg_collation_collname,
									 Anum_pg_collation_collnamespace,
									 Anum_pg_collation_collowner,
									 ACL_KIND_COLLATION);

	heap_close(rel, RowExclusiveLock);

	return oldNspOid;
}
