/*
 *	tablespace.c
 *
 *	tablespace functions
 *
 *	Copyright (c) 2010-2026, PostgreSQL Global Development Group
 *	src/bin/pg_upgrade/tablespace.c
 */

#include "postgres_fe.h"

#include "pg_upgrade.h"

static void get_tablespace_paths(void);
static void set_tablespace_directory_suffix(ClusterInfo *cluster);


void
init_tablespaces(void)
{
	get_tablespace_paths();

	set_tablespace_directory_suffix(&old_cluster);
	set_tablespace_directory_suffix(&new_cluster);

	if (old_cluster.num_tablespaces > 0 &&
		strcmp(old_cluster.tablespace_suffix, new_cluster.tablespace_suffix) == 0)
	{
		for (int i = 0; i < old_cluster.num_tablespaces; i++)
		{
			/*
			 * In-place tablespaces are okay for same-version upgrades because
			 * their paths will differ between clusters.
			 */
			if (strcmp(old_cluster.tablespaces[i], new_cluster.tablespaces[i]) == 0)
				pg_fatal("Cannot upgrade to/from the same system catalog version when\n"
						 "using tablespaces.");
		}
	}
}


/*
 * get_tablespace_paths()
 *
 * Scans pg_tablespace and returns a malloc'ed array of all tablespace
 * paths. It's the caller's responsibility to free the array.
 */
static void
get_tablespace_paths(void)
{
	PGconn	   *conn = connectToServer(&old_cluster, "template1");
	PGresult   *res;
	int			tblnum;
	int			i_spclocation;
	char		query[QUERY_ALLOC];

	snprintf(query, sizeof(query),
			 "SELECT pg_catalog.pg_tablespace_location(oid) AS spclocation "
			 "FROM	pg_catalog.pg_tablespace "
			 "WHERE	spcname != 'pg_default' AND "
			 "		spcname != 'pg_global'");

	res = executeQueryOrDie(conn, "%s", query);

	old_cluster.num_tablespaces = PQntuples(res);
	new_cluster.num_tablespaces = PQntuples(res);

	if (PQntuples(res) != 0)
	{
		old_cluster.tablespaces =
			(char **) pg_malloc(old_cluster.num_tablespaces * sizeof(char *));
		new_cluster.tablespaces =
			(char **) pg_malloc(new_cluster.num_tablespaces * sizeof(char *));
	}
	else
	{
		old_cluster.tablespaces = NULL;
		new_cluster.tablespaces = NULL;
	}

	i_spclocation = PQfnumber(res, "spclocation");

	for (tblnum = 0; tblnum < old_cluster.num_tablespaces; tblnum++)
	{
		struct stat statBuf;
		char	   *spcloc = PQgetvalue(res, tblnum, i_spclocation);

		/*
		 * For now, we do not expect non-in-place tablespaces to move during
		 * upgrade.  If that changes, it will likely become necessary to run
		 * the above query on the new cluster, too.
		 *
		 * pg_tablespace_location() returns absolute paths for non-in-place
		 * tablespaces and relative paths for in-place ones, so we use
		 * is_absolute_path() to distinguish between them.
		 */
		if (is_absolute_path(PQgetvalue(res, tblnum, i_spclocation)))
		{
			old_cluster.tablespaces[tblnum] = pg_strdup(spcloc);
			new_cluster.tablespaces[tblnum] = old_cluster.tablespaces[tblnum];
		}
		else
		{
			old_cluster.tablespaces[tblnum] = psprintf("%s/%s", old_cluster.pgdata, spcloc);
			new_cluster.tablespaces[tblnum] = psprintf("%s/%s", new_cluster.pgdata, spcloc);
		}

		/*
		 * Check that the tablespace path exists and is a directory.
		 * Effectively, this is checking only for tables/indexes in
		 * non-existent tablespace directories.  Databases located in
		 * non-existent tablespaces already throw a backend error.
		 * Non-existent tablespace directories can occur when a data directory
		 * that contains user tablespaces is moved as part of pg_upgrade
		 * preparation and the symbolic links are not updated.
		 */
		if (stat(old_cluster.tablespaces[tblnum], &statBuf) != 0)
		{
			if (errno == ENOENT)
				report_status(PG_FATAL,
							  "tablespace directory \"%s\" does not exist",
							  old_cluster.tablespaces[tblnum]);
			else
				report_status(PG_FATAL,
							  "could not stat tablespace directory \"%s\": %m",
							  old_cluster.tablespaces[tblnum]);
		}
		if (!S_ISDIR(statBuf.st_mode))
			report_status(PG_FATAL,
						  "tablespace path \"%s\" is not a directory",
						  old_cluster.tablespaces[tblnum]);
	}

	PQclear(res);

	PQfinish(conn);
}


static void
set_tablespace_directory_suffix(ClusterInfo *cluster)
{
	/* This cluster has a version-specific subdirectory */

	/* The leading slash is needed to start a new directory. */
	cluster->tablespace_suffix = psprintf("/PG_%s_%d",
										  cluster->major_version_str,
										  cluster->controldata.cat_ver);
}
