/*-------------------------------------------------------------------------
 * relation_stats.c
 *
 *	  PostgreSQL relation statistics manipulation
 *
 * Code supporting the direct import of relation statistics, similar to
 * what is done by the ANALYZE command.
 *
 * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *       src/backend/statistics/relation_stats.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/heapam.h"
#include "catalog/indexing.h"
#include "statistics/stat_utils.h"
#include "utils/fmgroids.h"
#include "utils/fmgrprotos.h"
#include "utils/syscache.h"

#define DEFAULT_RELPAGES Int32GetDatum(0)
#define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
#define DEFAULT_RELALLVISIBLE Int32GetDatum(0)

/*
 * Positional argument numbers, names, and types for
 * relation_statistics_update().
 */

enum relation_stats_argnum
{
	RELATION_ARG = 0,
	RELPAGES_ARG,
	RELTUPLES_ARG,
	RELALLVISIBLE_ARG,
	NUM_RELATION_STATS_ARGS
};

static struct StatsArgInfo relarginfo[] =
{
	[RELATION_ARG] = {"relation", REGCLASSOID},
	[RELPAGES_ARG] = {"relpages", INT4OID},
	[RELTUPLES_ARG] = {"reltuples", FLOAT4OID},
	[RELALLVISIBLE_ARG] = {"relallvisible", INT4OID},
	[NUM_RELATION_STATS_ARGS] = {0}
};

static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
									   bool inplace);

/*
 * Internal function for modifying statistics for a relation.
 */
static bool
relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
{
	Oid			reloid;
	Relation	crel;
	int32		relpages = DEFAULT_RELPAGES;
	bool		update_relpages = false;
	float		reltuples = DEFAULT_RELTUPLES;
	bool		update_reltuples = false;
	int32		relallvisible = DEFAULT_RELALLVISIBLE;
	bool		update_relallvisible = false;
	bool		result = true;

	if (!PG_ARGISNULL(RELPAGES_ARG))
	{
		relpages = PG_GETARG_INT32(RELPAGES_ARG);

		/*
		 * Partitioned tables may have relpages=-1. Note: for relations with
		 * no storage, relpages=-1 is not used consistently, but must be
		 * supported here.
		 */
		if (relpages < -1)
		{
			ereport(elevel,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("relpages cannot be < -1")));
			result = false;
		}
		else
			update_relpages = true;
	}

	if (!PG_ARGISNULL(RELTUPLES_ARG))
	{
		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);

		if (reltuples < -1.0)
		{
			ereport(elevel,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("reltuples cannot be < -1.0")));
			result = false;
		}
		else
			update_reltuples = true;
	}

	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
	{
		relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);

		if (relallvisible < 0)
		{
			ereport(elevel,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("relallvisible cannot be < 0")));
			result = false;
		}
		else
			update_relallvisible = true;
	}

	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
	reloid = PG_GETARG_OID(RELATION_ARG);

	if (RecoveryInProgress())
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("recovery is in progress"),
				 errhint("Statistics cannot be modified during recovery.")));

	stats_lock_check_privileges(reloid);

	/*
	 * Take RowExclusiveLock on pg_class, consistent with
	 * vac_update_relstats().
	 */
	crel = table_open(RelationRelationId, RowExclusiveLock);

	if (inplace)
	{
		HeapTuple	ctup = NULL;
		ScanKeyData key[1];
		Form_pg_class pgcform;
		void	   *inplace_state = NULL;
		bool		dirty = false;

		ScanKeyInit(&key[0], Anum_pg_class_oid, BTEqualStrategyNumber, F_OIDEQ,
					ObjectIdGetDatum(reloid));
		systable_inplace_update_begin(crel, ClassOidIndexId, true, NULL, 1, key,
									  &ctup, &inplace_state);
		if (!HeapTupleIsValid(ctup))
			elog(ERROR, "pg_class entry for relid %u vanished while updating statistics",
				 reloid);
		pgcform = (Form_pg_class) GETSTRUCT(ctup);

		if (update_relpages && pgcform->relpages != relpages)
		{
			pgcform->relpages = relpages;
			dirty = true;
		}
		if (update_reltuples && pgcform->reltuples != reltuples)
		{
			pgcform->reltuples = reltuples;
			dirty = true;
		}
		if (update_relallvisible && pgcform->relallvisible != relallvisible)
		{
			pgcform->relallvisible = relallvisible;
			dirty = true;
		}

		if (dirty)
			systable_inplace_update_finish(inplace_state, ctup);
		else
			systable_inplace_update_cancel(inplace_state);

		heap_freetuple(ctup);
	}
	else
	{
		TupleDesc	tupdesc = RelationGetDescr(crel);
		HeapTuple	ctup;
		Form_pg_class pgcform;
		int			replaces[3] = {0};
		Datum		values[3] = {0};
		bool		nulls[3] = {0};
		int			nreplaces = 0;

		ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
		if (!HeapTupleIsValid(ctup))
		{
			ereport(elevel,
					(errcode(ERRCODE_OBJECT_IN_USE),
					 errmsg("pg_class entry for relid %u not found", reloid)));
			table_close(crel, RowExclusiveLock);
			return false;
		}
		pgcform = (Form_pg_class) GETSTRUCT(ctup);

		if (update_relpages && relpages != pgcform->relpages)
		{
			replaces[nreplaces] = Anum_pg_class_relpages;
			values[nreplaces] = Int32GetDatum(relpages);
			nreplaces++;
		}

		if (update_reltuples && reltuples != pgcform->reltuples)
		{
			replaces[nreplaces] = Anum_pg_class_reltuples;
			values[nreplaces] = Float4GetDatum(reltuples);
			nreplaces++;
		}

		if (update_relallvisible && relallvisible != pgcform->relallvisible)
		{
			replaces[nreplaces] = Anum_pg_class_relallvisible;
			values[nreplaces] = Int32GetDatum(relallvisible);
			nreplaces++;
		}

		if (nreplaces > 0)
		{
			HeapTuple	newtup;

			newtup = heap_modify_tuple_by_cols(ctup, tupdesc, nreplaces,
											   replaces, values, nulls);
			CatalogTupleUpdate(crel, &newtup->t_self, newtup);
			heap_freetuple(newtup);
		}

		ReleaseSysCache(ctup);
	}

	/* release the lock, consistent with vac_update_relstats() */
	table_close(crel, RowExclusiveLock);

	CommandCounterIncrement();

	return result;
}

/*
 * Set statistics for a given pg_class entry.
 */
Datum
pg_set_relation_stats(PG_FUNCTION_ARGS)
{
	relation_statistics_update(fcinfo, ERROR, false);
	PG_RETURN_VOID();
}

/*
 * Clear statistics for a given pg_class entry; that is, set back to initial
 * stats for a newly-created table.
 */
Datum
pg_clear_relation_stats(PG_FUNCTION_ARGS)
{
	LOCAL_FCINFO(newfcinfo, 4);

	InitFunctionCallInfoData(*newfcinfo, NULL, 4, InvalidOid, NULL, NULL);

	newfcinfo->args[0].value = PG_GETARG_OID(0);
	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
	newfcinfo->args[1].value = DEFAULT_RELPAGES;
	newfcinfo->args[1].isnull = false;
	newfcinfo->args[2].value = DEFAULT_RELTUPLES;
	newfcinfo->args[2].isnull = false;
	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
	newfcinfo->args[3].isnull = false;

	relation_statistics_update(newfcinfo, ERROR, false);
	PG_RETURN_VOID();
}

Datum
pg_restore_relation_stats(PG_FUNCTION_ARGS)
{
	LOCAL_FCINFO(positional_fcinfo, NUM_RELATION_STATS_ARGS);
	bool		result = true;

	InitFunctionCallInfoData(*positional_fcinfo, NULL,
							 NUM_RELATION_STATS_ARGS,
							 InvalidOid, NULL, NULL);

	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,
										  relarginfo, WARNING))
		result = false;

	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
		result = false;

	PG_RETURN_BOOL(result);
}
