/*-------------------------------------------------------------------------
 *
 * deparse_table_stmts.c
 *	  All routines to deparse table statements.
 *	  This file contains all entry points specific for table statement deparsing as well as
 *	  functions that are currently only used for deparsing of the table statements.
 *
 * Copyright (c) Citus Data, Inc.
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "catalog/heap.h"
#include "commands/defrem.h"
#include "commands/tablecmds.h"
#include "nodes/nodes.h"
#include "nodes/parsenodes.h"
#include "parser/parse_expr.h"
#include "parser/parse_relation.h"
#include "parser/parse_type.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/ruleutils.h"

#include "distributed/commands.h"
#include "distributed/deparser.h"
#include "distributed/namespace_utils.h"
#include "distributed/version_compat.h"

static void AppendAlterTableSchemaStmt(StringInfo buf, AlterObjectSchemaStmt *stmt);
static void AppendAlterTableStmt(StringInfo buf, AlterTableStmt *stmt);
static void AppendAlterTableCmd(StringInfo buf, AlterTableCmd *alterTableCmd,
								AlterTableStmt *stmt);
static void AppendAlterTableCmdAddColumn(StringInfo buf, AlterTableCmd *alterTableCmd,
										 AlterTableStmt *stmt);
static void AppendAlterTableCmdDropConstraint(StringInfo buf,
											  AlterTableCmd *alterTableCmd);

char *
DeparseAlterTableSchemaStmt(Node *node)
{
	AlterObjectSchemaStmt *stmt = castNode(AlterObjectSchemaStmt, node);
	StringInfoData str = { 0 };
	initStringInfo(&str);

	Assert(stmt->objectType == OBJECT_TABLE || stmt->objectType == OBJECT_FOREIGN_TABLE);

	AppendAlterTableSchemaStmt(&str, stmt);
	return str.data;
}


static void
AppendAlterTableSchemaStmt(StringInfo buf, AlterObjectSchemaStmt *stmt)
{
	Assert(stmt->objectType == OBJECT_TABLE || stmt->objectType == OBJECT_FOREIGN_TABLE);

	bool isForeignTable = stmt->objectType == OBJECT_FOREIGN_TABLE;
	appendStringInfo(buf, "ALTER %sTABLE ", isForeignTable ? "FOREIGN " : "");
	if (stmt->missing_ok)
	{
		appendStringInfo(buf, "IF EXISTS ");
	}
	char *tableName = quote_qualified_identifier(stmt->relation->schemaname,
												 stmt->relation->relname);
	const char *newSchemaName = quote_identifier(stmt->newschema);
	appendStringInfo(buf, "%s SET SCHEMA %s;", tableName, newSchemaName);
}


/*
 * DeparseAlterTableStmt builds and returns a string representing the
 * AlterTableStmt where the object acted upon is of kind OBJECT_TABLE
 */
char *
DeparseAlterTableStmt(Node *node)
{
	AlterTableStmt *stmt = castNode(AlterTableStmt, node);
	StringInfoData str = { 0 };
	initStringInfo(&str);

	Assert(stmt->objtype == OBJECT_TABLE);

	AppendAlterTableStmt(&str, stmt);
	return str.data;
}


/*
 * AppendAlterTableStmt builds and returns an SQL command representing an
 * ALTER TABLE statement from given AlterTableStmt object where the object
 * acted upon is of kind OBJECT_TABLE
 */
static void
AppendAlterTableStmt(StringInfo buf, AlterTableStmt *stmt)
{
	const char *identifier = quote_qualified_identifier(stmt->relation->schemaname,
														stmt->relation->relname);
	ListCell *cmdCell = NULL;

	Assert(stmt->objtype == OBJECT_TABLE);

	appendStringInfo(buf, "ALTER TABLE %s", identifier);
	foreach(cmdCell, stmt->cmds)
	{
		if (cmdCell != list_head(stmt->cmds))
		{
			appendStringInfoString(buf, ", ");
		}

		AlterTableCmd *alterTableCmd = castNode(AlterTableCmd, lfirst(cmdCell));
		AppendAlterTableCmd(buf, alterTableCmd, stmt);
	}

	appendStringInfoString(buf, ";");
}


/*
 * AppendColumnNameList converts a list of columns into comma separated string format
 * (colname_1, colname_2, .., colname_n).
 */
void
AppendColumnNameList(StringInfo buf, List *columns)
{
	appendStringInfoString(buf, " (");

	ListCell *lc;
	bool firstkey = true;

	foreach(lc, columns)
	{
		if (firstkey == false)
		{
			appendStringInfoString(buf, ", ");
		}

		appendStringInfo(buf, "%s", quote_identifier(strVal(lfirst(lc))));
		firstkey = false;
	}

	appendStringInfoString(buf, " )");
}


/*
 * AppendAlterTableCmdConstraint builds a string required to create given
 * constraint as part of an ADD CONSTRAINT or an ADD COLUMN subcommand,
 * and appends it to the buf.
 */
static void
AppendAlterTableCmdConstraint(StringInfo buf, Constraint *constraint,
							  AlterTableStmt *stmt, AlterTableType subtype)
{
	if (subtype != AT_AddConstraint && subtype != AT_AddColumn)
	{
		ereport(ERROR, (errmsg("Unsupported alter table subtype: %d", (int) subtype)));
	}

	/* Need to deparse the alter table constraint command only if we are adding a constraint name.*/
	if (constraint->conname == NULL)
	{
		ereport(ERROR, (errmsg(
							"Constraint name can not be NULL when deparsing the constraint.")));
	}

	if (subtype == AT_AddConstraint)
	{
		appendStringInfoString(buf, " ADD CONSTRAINT ");
	}
	else
	{
		appendStringInfoString(buf, " CONSTRAINT ");
	}

	appendStringInfo(buf, "%s ", quote_identifier(constraint->conname));

	/* postgres version >= PG15
	 * UNIQUE [ NULLS [ NOT ] DISTINCT ] ( column_name [, ... ] ) [ INCLUDE ( column_name [, ...]) ]
	 * postgres version < PG15
	 * UNIQUE ( column_name [, ... ] ) [ INCLUDE ( column_name [, ...]) ]
	 * PRIMARY KEY ( column_name [, ... ] ) [ INCLUDE ( column_name [, ...]) ]
	 */
	if (constraint->contype == CONSTR_PRIMARY || constraint->contype == CONSTR_UNIQUE)
	{
		if (constraint->contype == CONSTR_PRIMARY)
		{
			appendStringInfoString(buf,
								   " PRIMARY KEY ");
		}
		else
		{
			appendStringInfoString(buf, " UNIQUE");

			if (constraint->nulls_not_distinct == true)
			{
				appendStringInfoString(buf, " NULLS NOT DISTINCT");
			}
		}

		if (subtype == AT_AddConstraint)
		{
			AppendColumnNameList(buf, constraint->keys);
		}

		if (constraint->including != NULL)
		{
			appendStringInfoString(buf, " INCLUDE ");

			AppendColumnNameList(buf, constraint->including);
		}

		if (constraint->options != NIL)
		{
			appendStringInfoString(buf, " WITH(");

			ListCell *defListCell;
			foreach(defListCell, constraint->options)
			{
				DefElem *def = (DefElem *) lfirst(defListCell);

				bool first = (defListCell == list_head(constraint->options));
				appendStringInfo(buf, "%s%s=%s", first ? "" : ",",
								 quote_identifier(def->defname),
								 quote_literal_cstr(defGetString(def)));
			}

			appendStringInfoChar(buf, ')');
		}
	}
	else if (constraint->contype == CONSTR_EXCLUSION)
	{
		/*
		 * This block constructs the EXCLUDE clause which is in the following form:
		 * EXCLUDE [ USING index_method ] ( exclude_element WITH operator [, ... ] )
		 */
		appendStringInfoString(buf, " EXCLUDE ");

		if (constraint->access_method != NULL)
		{
			appendStringInfoString(buf, "USING ");
			appendStringInfo(buf, "%s ", quote_identifier(
								 constraint->access_method));
		}

		appendStringInfoString(buf, " (");

		ListCell *lc;
		bool firstOp = true;

		foreach(lc, constraint->exclusions)
		{
			List *pair = (List *) lfirst(lc);

			Assert(list_length(pair) == 2);
			IndexElem *elem = linitial_node(IndexElem, pair);
			List *opname = lsecond_node(List, pair);
			if (firstOp == false)
			{
				appendStringInfoString(buf, " ,");
			}

			ListCell *lc2;

			foreach(lc2, opname)
			{
				appendStringInfo(buf, "%s WITH %s", quote_identifier(elem->name),
								 strVal(lfirst(lc2)));
			}

			firstOp = false;
		}

		appendStringInfoString(buf, " )");
	}
	else if (constraint->contype == CONSTR_CHECK)
	{
		if (subtype == AT_AddColumn)
		{
			/*
			 * Preprocess should've rejected deparsing such an ALTER TABLE
			 * command but be on the safe side.
			 */
			ereport(ERROR, (errmsg("cannot add check constraint to column by "
								   "using ADD COLUMN command"),
							errhint("Consider using ALTER TABLE ... ADD CONSTRAINT "
									"... CHECK command after adding the column")));
		}

		LOCKMODE lockmode = AlterTableGetLockLevel(stmt->cmds);
		Oid leftRelationId = AlterTableLookupRelation(stmt, lockmode);

		/* To be able to use deparse_expression function, which creates an expression string,
		 * the expression should be provided in its cooked form. We transform the raw expression
		 * to cooked form.
		 */
		ParseState *pstate = make_parsestate(NULL);
		Relation relation = table_open(leftRelationId, AccessShareLock);

		/* Add table name to the name space in  parse state. Otherwise column names
		 * cannot be found.
		 */
		AddRangeTableEntryToQueryCompat(pstate, relation);

		Node *exprCooked = transformExpr(pstate, constraint->raw_expr,

										 EXPR_KIND_CHECK_CONSTRAINT);

		char *relationName = get_rel_name(leftRelationId);
		List *relationCtx = deparse_context_for(relationName, leftRelationId);

		char *exprSql = deparse_expression(exprCooked, relationCtx, false, false);

		relation_close(relation, NoLock);

		appendStringInfo(buf, " CHECK (%s)", exprSql);

		if (constraint->is_no_inherit)
		{
			appendStringInfo(buf, " NO INHERIT");
		}
	}
	else if (constraint->contype == CONSTR_FOREIGN)
	{
		if (subtype == AT_AddConstraint)
		{
			appendStringInfoString(buf, " FOREIGN KEY");

			AppendColumnNameList(buf, constraint->fk_attrs);
		}

		appendStringInfoString(buf, " REFERENCES");

		appendStringInfo(buf, " %s", quote_qualified_identifier(
							 constraint->pktable->schemaname,
							 constraint->pktable->relname));

		if (list_length(constraint->pk_attrs) > 0)
		{
			AppendColumnNameList(buf, constraint->pk_attrs);
		}

		/* Append supported options if provided */

		/* FKCONSTR_MATCH_SIMPLE is default. Append matchtype if not default */
		if (constraint->fk_matchtype == FKCONSTR_MATCH_FULL)
		{
			appendStringInfoString(buf, " MATCH FULL");
		}

		switch (constraint->fk_del_action)
		{
			case FKCONSTR_ACTION_SETDEFAULT:
			{
				appendStringInfoString(buf, " ON DELETE SET DEFAULT");
				break;
			}

			case FKCONSTR_ACTION_SETNULL:
			{
				appendStringInfoString(buf, " ON DELETE SET NULL");
				break;
			}

			case FKCONSTR_ACTION_NOACTION:
			{
				appendStringInfoString(buf, " ON DELETE NO ACTION");
				break;
			}

			case FKCONSTR_ACTION_RESTRICT:
			{
				appendStringInfoString(buf, " ON DELETE RESTRICT");
				break;
			}

			case FKCONSTR_ACTION_CASCADE:
			{
				appendStringInfoString(buf, " ON DELETE CASCADE");
				break;
			}

			default:
			{
				elog(ERROR, "unsupported FK delete action type: %d",
					 (int) constraint->fk_del_action);
				break;
			}
		}

		switch (constraint->fk_upd_action)
		{
			case FKCONSTR_ACTION_SETDEFAULT:
			{
				appendStringInfoString(buf, " ON UPDATE SET DEFAULT");
				break;
			}

			case FKCONSTR_ACTION_SETNULL:
			{
				appendStringInfoString(buf, " ON UPDATE SET NULL");
				break;
			}

			case FKCONSTR_ACTION_NOACTION:
			{
				appendStringInfoString(buf, " ON UPDATE NO ACTION");
				break;
			}

			case FKCONSTR_ACTION_RESTRICT:
			{
				appendStringInfoString(buf, " ON UPDATE RESTRICT");
				break;
			}

			case FKCONSTR_ACTION_CASCADE:
			{
				appendStringInfoString(buf, " ON UPDATE CASCADE");
				break;
			}

			default:
			{
				elog(ERROR, "unsupported FK update action type: %d",
					 (int) constraint->fk_upd_action);
				break;
			}
		}
	}

	/*
	 * For ADD CONSTRAINT subcommand, FOREIGN KEY and CHECK constraints migth
	 * have NOT VALID option.
	 *
	 * Note that skip_validation might be true for an ADD COLUMN too but this
	 * is not because Postgres supports this but because Citus sets this flag
	 * to true for foreign key constraints added via ADD COLUMN. So we don't
	 * check for skip_validation for ADD COLUMN subcommand.
	 */
	if (subtype == AT_AddConstraint && constraint->skip_validation)
	{
		appendStringInfoString(buf, " NOT VALID ");
	}

	if (subtype == AT_AddColumn &&
		(constraint->deferrable || constraint->initdeferred))
	{
		/*
		 * For ADD COLUMN subcommand, the fact that whether given constraint
		 * is deferrable or initially deferred is indicated by another Constraint
		 * object, not via deferrable / initdeferred fields.
		 */
		ereport(ERROR, (errmsg("unexpected value set for deferrable/initdeferred "
							   "field for an ADD COLUMN subcommand")));
	}

	if (constraint->deferrable)
	{
		appendStringInfoString(buf, " DEFERRABLE");

		if (constraint->initdeferred)
		{
			appendStringInfoString(buf, " INITIALLY DEFERRED");
		}
	}
}


/*
 * AppendAlterTableCmd builds and appends to the given buffer a command
 * from given AlterTableCmd object. Currently supported commands are of type
 * AT_AddColumn, AT_SetNotNull and AT_AddConstraint {PRIMARY KEY, UNIQUE, EXCLUDE}.
 */
static void
AppendAlterTableCmd(StringInfo buf, AlterTableCmd *alterTableCmd, AlterTableStmt *stmt)
{
	switch (alterTableCmd->subtype)
	{
		case AT_AddColumn:
		{
			AppendAlterTableCmdAddColumn(buf, alterTableCmd, stmt);
			break;
		}

		case AT_DropConstraint:
		{
			AppendAlterTableCmdDropConstraint(buf, alterTableCmd);
			break;
		}

		case AT_AddConstraint:
		{
			Constraint *constraint = (Constraint *) alterTableCmd->def;

			/* We need to deparse ALTER TABLE ... ADD {PRIMARY KEY, UNIQUE, EXCLUSION} commands into
			 * ALTER TABLE ... ADD CONSTRAINT <conname> {PRIMARY KEY, UNIQUE, EXCLUSION} ... format to be able
			 * add a constraint name.
			 */
			if (ConstrTypeCitusCanDefaultName(constraint->contype))
			{
				AppendAlterTableCmdConstraint(buf, constraint, stmt, AT_AddConstraint);
				break;
			}
		}

		/* fallthrough */

		default:
		{
			ereport(ERROR, (errmsg("unsupported subtype for alter table command"),
							errdetail("sub command type: %d", alterTableCmd->subtype)));
		}
	}
}


/*
 * GeneratedWhenStr returns the char representation of given generated_when
 * value.
 */
static const char *
GeneratedWhenStr(char generatedWhen)
{
	switch (generatedWhen)
	{
		case 'a':
		{
			return "ALWAYS";
		}

		case 'd':
		{
			return "BY DEFAULT";
		}

		default:
			ereport(ERROR, (errmsg("unrecognized generated_when: %d",
								   generatedWhen)));
	}
}


/*
 * DeparseRawExprForColumnDefault returns string representation of given
 * rawExpr based on given column type information.
 */
static char *
DeparseRawExprForColumnDefault(Oid relationId, Oid columnTypeId, int32 columnTypeMod,
							   char *columnName, char attgenerated, Node *rawExpr)
{
	ParseState *pstate = make_parsestate(NULL);
	Relation relation = RelationIdGetRelation(relationId);
	AddRangeTableEntryToQueryCompat(pstate, relation);

	Node *defaultExpr = cookDefault(pstate, rawExpr,
									columnTypeId, columnTypeMod,
									columnName, attgenerated);

	List *deparseContext = deparse_context_for(get_rel_name(relationId), relationId);

	int saveNestLevel = PushEmptySearchPath();
	char *defaultExprStr = deparse_expression(defaultExpr, deparseContext, false, false);
	PopEmptySearchPath(saveNestLevel);

	RelationClose(relation);

	return defaultExprStr;
}


/*
 * AppendAlterTableCmd builds and appends to the given buffer an AT_AddColumn command
 * from given AlterTableCmd object in the form ADD COLUMN ...
 */
static void
AppendAlterTableCmdAddColumn(StringInfo buf, AlterTableCmd *alterTableCmd,
							 AlterTableStmt *stmt)
{
	Assert(alterTableCmd->subtype == AT_AddColumn);

	Oid relationId = AlterTableLookupRelation(stmt, NoLock);

	appendStringInfoString(buf, " ADD COLUMN ");

	if (alterTableCmd->missing_ok)
	{
		appendStringInfoString(buf, "IF NOT EXISTS ");
	}

	ColumnDef *columnDefinition = (ColumnDef *) alterTableCmd->def;

	if (columnDefinition->colname)
	{
		appendStringInfo(buf, "%s ", quote_identifier(columnDefinition->colname));
	}

	int32 typmod = 0;
	Oid typeOid = InvalidOid;
	bits16 formatFlags = FORMAT_TYPE_TYPEMOD_GIVEN | FORMAT_TYPE_FORCE_QUALIFY;
	typenameTypeIdAndMod(NULL, columnDefinition->typeName, &typeOid, &typmod);
	appendStringInfo(buf, "%s", format_type_extended(typeOid, typmod,
													 formatFlags));

	if (columnDefinition->compression)
	{
		appendStringInfo(buf, " COMPRESSION %s",
						 quote_identifier(columnDefinition->compression));
	}

	Oid collationOid = GetColumnDefCollation(NULL, columnDefinition, typeOid);
	if (OidIsValid(collationOid))
	{
		const char *identifier = FormatCollateBEQualified(collationOid);
		appendStringInfo(buf, " COLLATE %s", identifier);
	}

	ListCell *constraintCell = NULL;
	foreach(constraintCell, columnDefinition->constraints)
	{
		Constraint *constraint = (Constraint *) lfirst(constraintCell);

		if (constraint->contype == CONSTR_NOTNULL)
		{
			appendStringInfoString(buf, " NOT NULL");
		}
		else if (constraint->contype == CONSTR_NULL)
		{
			appendStringInfoString(buf, " NULL");
		}
		else if (constraint->contype == CONSTR_DEFAULT)
		{
			char attgenerated = '\0';
			appendStringInfo(buf, " DEFAULT %s",
							 DeparseRawExprForColumnDefault(relationId, typeOid, typmod,
															columnDefinition->colname,
															attgenerated,
															constraint->raw_expr));
		}
		else if (constraint->contype == CONSTR_IDENTITY)
		{
			/*
			 * Citus doesn't support adding identity columns via ALTER TABLE,
			 * so we don't bother teaching the deparser about them.
			 */
			ereport(ERROR, (errmsg("unexpectedly found identity column "
								   "definition in ALTER TABLE command")));
		}
		else if (constraint->contype == CONSTR_GENERATED)
		{
			char attgenerated = 's';
			appendStringInfo(buf, " GENERATED %s AS (%s) STORED",
							 GeneratedWhenStr(constraint->generated_when),
							 DeparseRawExprForColumnDefault(relationId, typeOid, typmod,
															columnDefinition->colname,
															attgenerated,
															constraint->raw_expr));
		}
		else if (constraint->contype == CONSTR_CHECK ||
				 constraint->contype == CONSTR_PRIMARY ||
				 constraint->contype == CONSTR_UNIQUE ||
				 constraint->contype == CONSTR_EXCLUSION ||
				 constraint->contype == CONSTR_FOREIGN)
		{
			AppendAlterTableCmdConstraint(buf, constraint, stmt, AT_AddColumn);
		}
		else if (constraint->contype == CONSTR_ATTR_DEFERRABLE)
		{
			appendStringInfoString(buf, " DEFERRABLE");
		}
		else if (constraint->contype == CONSTR_ATTR_NOT_DEFERRABLE)
		{
			appendStringInfoString(buf, " NOT DEFERRABLE");
		}
		else if (constraint->contype == CONSTR_ATTR_DEFERRED)
		{
			appendStringInfoString(buf, " INITIALLY DEFERRED");
		}
		else if (constraint->contype == CONSTR_ATTR_IMMEDIATE)
		{
			appendStringInfoString(buf, " INITIALLY IMMEDIATE");
		}
		else
		{
			ereport(ERROR, (errmsg("unsupported constraint type"),
							errdetail("constraint type: %d", constraint->contype)));
		}
	}
}


/*
 * AppendAlterTableCmdDropConstraint builds and appends to the given buffer an
 * AT_DropConstraint command from given AlterTableCmd object in the form
 * DROP CONSTRAINT ...
 */
static void
AppendAlterTableCmdDropConstraint(StringInfo buf, AlterTableCmd *alterTableCmd)
{
	appendStringInfoString(buf, " DROP CONSTRAINT");

	if (alterTableCmd->missing_ok)
	{
		appendStringInfoString(buf, " IF EXISTS");
	}

	appendStringInfo(buf, " %s", quote_identifier(alterTableCmd->name));

	if (alterTableCmd->behavior == DROP_CASCADE)
	{
		appendStringInfoString(buf, " CASCADE");
	}
}
